请考虑一系列简单和案例类,交叉编译到 JVM 和 Scala.js,它们混合使用属性与自动和手动定义的访问器方法。许多来自外部库,但都继承自询问者拥有的特定特征:
trait ContextuallyMutable { ... }
如何对继承自 ContextuallyMutable 的每个类的所有 setter/mutator 方法实施系统范围的约束?
有问题的约束将执行上下文分为两种类型:owner和client。如果从所有者上下文调用,setter 会正常执行,但在客户端上下文中它会启动远程过程调用。
作为一个粗略的例子,考虑一个简单的案例类:
case class PropTest(var i:Int) extends ContextuallyMutable
这会提示 Scala 编译器生成一个 setter/mutator 方法,例如:
def i_=(i0:Int):Unit = this._i = i0
为了满足约束,setter 方法应该改为:
def i_=(i0:Int):Unit = {
if (isOwnerContext) { // prohibit direct mutation in client contexts.
this._i = i0
} else tell(this, i0) // fire and forget message to owner context
}
在一个较小、协作较少的项目中,可以想象手动将约束强制写入每个类的每个 setter/mutator:
case class PropTest(private var _i:Int) extends ContextPropped {
def i:Int = _i
def i_=(i0:Int):Unit = {
if (isOwnerContext) wrapped.i = i0
else tell(this, i0)
}
}
...但是人们不应该想象这样乏味、容易出错的样板文件,以免给贡献库的维护者带来类似的乏味。
此外,手动覆盖的访问器方法与已建立的序列化规范冲突,因为 Scala 常规使用下划线:private var _propName:Type
用于手动定义的属性。
{ "i":42 } /* rewards intuition while */ { "_i":42 } // frightens it away
在不改变 Scala.js 类的源代码的情况下,如何将它的 setter/mutator 方法注入以调用上下文为条件的替代行为?
更广泛地说:Scala.js 生态系统是否提供任何方式来增加或替换 scala 类的属性字段的自动生成的 setter/mutator 方法?
编辑:进一步的研究发现了一些可能性和相关的子问题:
- 依赖注入框架的拦截器。Scala.js 版本的MacWire拦截器能否满足此约束?文档并没有明确说明拦截器是否可以针对给定特征的所有继承者的所有 setter/mutator。如果 MacWire 或其他兼容 Scala.js 的 DI 框架可以提供此功能,那么 IOC 看起来是一个相对轻松的解决方案,除了将繁重的框架依赖项添加到库中的糟糕方式。
- 包装纸。以下工作,除了它需要重构对 PropTest 的每个实例的每个引用,并强制该库的用户有意识地区分 Client 和 Owner 上下文。
case class PropTest(var i:Int): extends ContextPropped
class PropTestWrapper(private val wrapped:PropTest){
def i:Int = wrapped.i
def i_=(i0:Int):Unit = {
if (isOwnerContext) wrapped.i = i0
else tell(this, i0)
}
}
- 子类。扩展原始类型很好地解决了这个问题,只是它依赖于许多繁琐的样板,扩展了案例类,并且不编译。
class PropTestSubclass(var _i:Int) extends PropTest(_i) {
// Compiler Error: mutable variable cannot be overridden
override def i_=(i0:Int):Unit = {
if (isOwnerContext) wrapped.i = i0
else tell(this, i0)
}
}
- 包装的子类或 DIY 代理。更有吸引力的是,通过将包装器与子类结合起来,我们最大限度地提高了包装器和被包装类之间的类型兼容性。但是,我们还结合了我们希望避免的所有惩罚:编译器错误、乏味和样板,并添加了扩展案例类的禁忌实践:
class PropTestProxy(private val wrapped:PropTest) extends PropTest(wrapped.i) {
// Compiler Error: mutable variable cannot be overridden
override def i_=(i0:Int):Unit = {
if (isOwnerContext) wrapped.i = i0
else tell(this, i0)
}
}
当然,这些编译器错误在将原来的case类改写为:
case class PropTest(private var _i:Int) extends ContextPropped {
def i:Int = _i
def i_=(i0:Int):Unit = this._i = i0
}
...但是,正如前面提到的:这种干预需要更多的打字,而不是从头开始完全重写整个项目。
另一种方法更直接地关注委托/代理实例的概念。代理类的主要挑战似乎源于一种类型合并问题。一个理想的代理与它所调解的类型没有用户可区分的区别。
- Java 的Proxy类及其InvocationHandler。完美,只是它只代理 Java 接口并且依赖于 Scala.js 无法支持的反射。
- Scala 的代理特性。仅限于 hashCode、equals 和 toString 方法并已弃用。
- 由cmhteixeira委托宏编译器插件 。非常接近,但是,就像 Java 的 Proxy 类一样,它只适用于接口(也是 trait)并且只促进手动方法覆盖。也许与其他宏一起使用,这可能有助于一个漂亮的解决方案,但也许一个类和该类的代理之间的类型差距永远不会完全关闭。
在 Scala 生态系统中,委托/代理能否以完全类型安全的方式代替其目标?cmhteixeira会怎么说?
其他编译器插件解决方案:
- Jakub Kozłowski的better-tostring和polyvariant指出了在编译时重写自动生成的 toString 方法的可能性,并且明确地不是手动覆盖的方法,但是由于对编译器插件的了解为零,询问者无法估计通过开发解决这个设计挑战的可行性一个自定义的 Scala 编译器插件。有什么想法吗,雅库布·科兹沃夫斯基?
感谢您的考虑!