在混合他们的同时保留特质个性

non*_*com 7 design-patterns scala traits

我想基于Scala特性创建一个带有一些特殊属性的enity系统.

主要思想是:所有组件都是从共同特征继承的特征:

trait Component
trait ComponentA extends Component
Run Code Online (Sandbox Code Playgroud)

有时,如果是更复杂的层次结构和相互依赖的组件,它可以像这样:

trait ComponentN extends ComponentM {
  self: ComponentX with ComponentY =>

  var a = 1
  var b = "hello"
}
Run Code Online (Sandbox Code Playgroud)

等等.我得出的结论是,Entity由于访问速度的原因,与每个组件相关的数据本身应该包含在内,而不是在一个或其他地方的某些存储器中.作为旁注 - 这也是为什么一切都是可变的,所以没有必要考虑不变性.

然后Entities创建,混合特征:

class Entity

class EntityANXY extends ComponentA
  with ComponentN
  with ComponentX
  with ComponentY
Run Code Online (Sandbox Code Playgroud)

这里一切都很好,但我有一个特殊的要求,我不知道如何满足代码.要求是这样的:

每个特征必须提供有助于以通用形式收集特征相关数据的编码方法(?),例如以JSON Map等形式,Map("a" -> "1", "b" -> "hello")以及将这样的地图(如果接收的话)转换回来的解码方法.与性状有关的变量.另外:1)所有混合特征的所有编码和解码方法都是按照任意顺序通过Entity方法调用的encode,decode(Map)并且2)应该可以通过指定特征类型单独调用,或者更好,通过像字符串参数decode("component-n", Map).

不可能使用具有相同名称的方法,因为它们会因阴影或覆盖而丢失.我可以想到一个解决方案,其中所有方法都存储在Map[String, Map[String, String] => Unit]for decode中,并Map[String, () => Map[String, String]]每个实体中进行编码.这可以工作 - 名字和一堆电话肯定是可用的.但是,这将导致在每个实体中存储相同的信息,这是不可接受的.

也可以将这些映射存储在伴随对象中,以便它不会在任何地方重复,encodedecode使用表示实体的特定实例的额外参数来调用对象和方法.

这个要求可能看起来很奇怪,但由于所需的速度和模块性,这是必要的.所有这些解决方案都很笨拙,我认为在Scala中有一个更好的惯用解决方案,或者我可能在这里缺少一些重要的架构模式.那么有没有比伴随对象更简单,更惯用的方法?

编辑:我认为聚合而不是继承可能解决这些问题,但代价是无法直接在实体上调用方法.

更新:探索Rex Kerr提出的非常有前途的方法,我偶然发现了一些阻碍的事情.以下是测试用例:

trait Component {
  def encode: Map[String, String]
  def decode(m: Map[String, String]) 
}

abstract class Entity extends Component // so as to enforce the two methods

trait ComponentA extends Component {
  var a = 10
  def encode: Map[String, String] = Map("a" -> a.toString)
  def decode(m: Map[String, String]) {
    println("ComponentA: decode " + m)
    m.get("a").collect{case aa => a = aa.toInt}
  }
}

trait ComponentB extends ComponentA {
  var b = 100
  override def encode: Map[String, String] = super.encode + ("b" -> b.toString)
  override def decode (m: Map[String, String]) {
    println("ComponentB: decoding " + m)
    super.decode(m)
    m.get("b").foreach{bb => b = bb.toInt}
  } 
}

trait ComponentC extends Component {
  var c = "hey!"
  def encode: Map[String, String] = Map("c" -> c)
  def decode(m: Map[String, String]) {
    println("ComponentC: decode " + m)
    m.get("c").collect{case cc => c = cc}
  }
}

trait ComponentD extends ComponentB with ComponentC {
  var d = 11.6f
  override def encode: Map[String, String] = super.encode + ("d" -> d.toString)
  override def decode(m: Map[String, String]) {
    println("ComponentD: decode " + m)
    super.decode(m)
    m.get("d").collect{case dd => d = dd.toFloat}
  }
}
Run Code Online (Sandbox Code Playgroud)

最后

class EntityA extends ComponentA with ComponentB with ComponentC with ComponentD
Run Code Online (Sandbox Code Playgroud)

以便

object Main {
  def main(args: Array[String]) {
    val ea = new EntityA
    val map = Map("a" -> "1", "b" -> "3", "c" -> "what?", "d" -> "11.24")
    println("BEFORE: " + ea.encode)
    ea.decode(map)
    println("AFTER: " + ea.encode)
  }
}
Run Code Online (Sandbox Code Playgroud)

这使:

BEFORE: Map(c -> hey!, d -> 11.6)
ComponentD: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24)
ComponentC: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24)
AFTER: Map(c -> what?, d -> 11.24)
Run Code Online (Sandbox Code Playgroud)

A和B组件不受影响,由继承分辨率切断.因此,此方法仅适用于某些层次结构案例.在这种情况下,我们看到它ComponentD已经遮蔽了其他一切.欢迎任何评论.

更新2:我在这里放置了回答这个问题的评论,以便更好地参考:"Scala线性化所有特征.应该有一个超级的东西将终止链.在你的情况下,这意味着,C并且A仍然应该调用super,并且Component应该是用无操作终止链的人." - 雷克斯克尔

Rex*_*err 5

特拉维斯有一个基本正确的答案; 不知道为什么他删除了它.但是,不管怎样,你可以,只要你愿意让你的编码方法需要一个额外的参数,并且,当你解码你很高兴刚才设置可变变量做没有太多的悲伤,而不是创建新的对象.(复杂的特征 - 在运行时有效堆叠的范围从难以实现.)

基本观察是,当您将特征链接在一起时,它定义了超类调用的层次结构.如果每个调用负责数据在特质,你会设定,只要你能找到一个办法让所有的数据备份.所以

trait T {
  def encodeMe(s: Seq[String]): Seq[String] = Seq()
  def encode = encodeMe(Seq())
}
trait A extends T {
  override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "A"
}
trait B extends T {
  override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "B"
}
Run Code Online (Sandbox Code Playgroud)

它有用吗?

scala> val a = new A with B
a: java.lang.Object with A with B = $anon$1@41a92be6

scala> a.encode
res8: Seq[String] = List(A, B)

scala> val b = new B with A
b: java.lang.Object with B with A = $anon$1@3774acff

scala> b.encode
res9: Seq[String] = List(B, A)
Run Code Online (Sandbox Code Playgroud)

确实!它不仅有效,而且您可以免费获得订单.

现在我们需要一种基于此编码设置变量的方法.在这里,我们遵循相同的模式 - 我们采取一些输入,并用它上升超级链.如果堆叠了很多特征,则可能需要将文本预解析为地图或过滤掉适用于当前特征的那些部分.如果没有,只需将所有内容传递给超级,然后自行设置.

trait T {
  var t = 0
  def decode(m: Map[String,Int]) { m.get("t").foreach{ ti => t = ti } }
}
trait C extends T {
  var c = 1
  override def decode(m: Map[String,Int]) { 
    super.decode(m); m.get("c").foreach{ ci => c = ci }
  }
}
trait D extends T {
  var d = 1
  override def decode(m: Map[String,Int]) {
    super.decode(m); m.get("d").foreach{ di => d = di }
  }
}
Run Code Online (Sandbox Code Playgroud)

这也像人们希望的那样起作用:

scala> val c = new C with D
c: java.lang.Object with C with D = $anon$1@549f9afb

scala> val d = new D with C
d: java.lang.Object with D with C = $anon$1@548ea21d

scala> c.decode(Map("c"->4,"d"->2,"t"->5))

scala> "%d %d %d".format(c.t,c.c,c.d)
res1: String = 5 4 2
Run Code Online (Sandbox Code Playgroud)

  • @noncom - Scala _linearizes_所有特征.应该有一个超级的东西,将终止链.在你的情况下,这意味着C和A仍然应该调用super,并且`Component`应该是用no-op终止链的那个. (2认同)