如何在Scala trait层面实现错误类withContext?避免样板代码
问题
我有一套错误继承体系,因为存在菱形继承结构,错误类必须扩展trait。很多场景下需要在错误生成位置的上层调用站点为其附加上下文,示例代码如下:
trait MyError{ def getMessage : String val context : String override def toString = context + getMessage def withContext(newContext : String) : MyError } case class IntError(value : Int, context : String = "") extends MyError{ def getMessage = s"$value is not a good enough int." def withContext(newContext : String) = this.copy(context = context+ newContext + "\n") } case class StringError(value : String, context: String = "") extends MyError{ def getMessage = s"Error Encountered: $value" def withContext(newContext : String) = this.copy(context = context + newContext+ "\n") } def bar (i : Int, b : Boolean) : Either[MyError, Int] = { if (b) then { if (i < 0) then Left(IntError(i)) else Right(i) } else Left(StringError("That needed to be True.")) } def foo() : Either[MyError, Int] = bar(-2, true) .left.map(_.withContext("Error calling bar from foo:")) def baz() : Either[MyError, Int] = bar(3, false) .left.map(_.withContext("Error calling bar from baz:")) println(foo().left.get) println(baz().left.get)
执行结果符合预期:
Error calling bar from foo: -2 is not a good enough int. Error calling bar from baz: Error Encountered: That needed to be True.
但因为case类数量很多,不想为每个类编写withContext的样板代码。请问能否将该方法的实现移至trait层面?额外约束/优化目标:
- 不可使用可变操作;
MyError需保留为trait(因存在菱形继承结构,可接受合理变通方案);- 若
context无需在每个case类中定义则更佳; withContext最好返回具体的case类类型而非通用的MyError(非必须)。
解决方案
方案1:泛型+自类型实现通用逻辑,统一管理context
通过泛型绑定和自类型约束,让trait MyError知晓子类是带context字段的case类,同时将context统一放在trait中,避免子类重复定义:
trait MyError[E <: MyError[E]] { self: E => def getMessage: String val context: String = "" override def toString = context + getMessage // 复用case类自动生成的copy方法,实现通用的withContext def withContext(newContext: String): E = { val updatedContext = context + newContext + "\n" this.copy(context = updatedContext) } // 定义copy方法签名,强制子类(case类)实现(case类会自动生成符合要求的copy) def copy(context: String): E } // 子类只需实现getMessage,无需手动编写withContext case class IntError(value: Int, override val context: String = "") extends MyError[IntError] { def getMessage = s"$value is not a good enough int." } case class StringError(value: String, override val context: String = "") extends MyError[StringError] { def getMessage = s"Error Encountered: $value" } // 原业务逻辑保持不变 def bar(i: Int, b: Boolean): Either[MyError[_], Int] = { if (b) { if (i < 0) Left(IntError(i)) else Right(i) } else Left(StringError("That needed to be True.")) } def foo(): Either[MyError[_], Int] = bar(-2, true) .left.map(_.withContext("Error calling bar from foo:")) def baz(): Either[MyError[_], Int] = bar(3, false) .left.map(_.withContext("Error calling bar from baz:")) println(foo().left.get) println(baz().left.get)
该方案满足所有约束:
- 无可变操作,通过
copy创建新实例实现上下文追加; MyError保持为trait,泛型+自类型可适配菱形继承场景;context仅在trait中定义,子类仅需按需override默认值;withContext返回具体case类类型,保证类型安全。
方案2:类型类解耦copy逻辑(可选)
如果不想让MyError带泛型,可通过类型类封装copy逻辑,进一步解耦:
trait MyError { def getMessage: String val context: String = "" override def toString = context + getMessage def withContext(newContext: String): MyError } // 定义类型类,处理带context字段的case类复制逻辑 trait ContextCopier[E <: MyError] { def copyWithContext(error: E, newContext: String): E } // 自动为符合条件的case类生成ContextCopier实例 object ContextCopier { implicit def forCaseClass[E <: MyError](implicit ev: E <:< { def copy(context: String): E }): ContextCopier[E] = (error: E, newContext: String) => { val updatedContext = error.context + newContext + "\n" error.copy(context = updatedContext) } } // 扩展MyError,提供withContext的默认实现 trait MyErrorWithContext extends MyError { self: E => def withContext(newContext: String)(implicit copier: ContextCopier[E]): E = copier.copyWithContext(this, newContext) } // 子类只需继承MyErrorWithContext,无需额外实现 case class IntError(value: Int, override val context: String = "") extends MyErrorWithContext { def getMessage = s"$value is not a good enough int." } case class StringError(value: String, override val context: String = "") extends MyErrorWithContext { def getMessage = s"Error Encountered: $value" } // 业务逻辑使用方式不变 def bar(i: Int, b: Boolean): Either[MyError, Int] = { if (b) { if (i < 0) Left(IntError(i)) else Right(i) } else Left(StringError("That needed to be True.")) } def foo(): Either[MyError, Int] = bar(-2, true) .left.map(_.withContext("Error calling bar from foo:")) def baz(): Either[MyError, Int] = bar(3, false) .left.map(_.withContext("Error calling bar from baz:")) println(foo().left.get) println(baz().left.get)
这个方案同样满足所有约束,且MyError的泛型被隐藏在类型类中,对调用者更透明。
内容的提问来源于stack exchange,提问作者Edward Peters
相关产品推荐
相关产品推荐

