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

时间:2012-08-22 11:19:59

标签: scala design-patterns traits

我想基于Scala特征创建一个具有一些特殊属性的enity系统。

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

trait Component
trait ComponentA extends Component

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

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

  var a = 1
  var b = "hello"
}

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

然后创建Entities,混合特征:

class Entity

class EntityANXY extends ComponentA
  with ComponentN
  with ComponentX
  with ComponentY

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

每个特征必须提供一种编码方法(?),以便于以通用形式收集与特征相关的数据,例如以JSON或Map的形式,如Map("a" -> "1", "b" -> "hello")和解码方法将这样的地图翻译,如果收到,则返回与特征相关的变量。另外:1)所有混合特征的所有编码和解码方法都是按Entity方法encodedecode(Map)和2)的任意顺序一组调用的。应该可以通过指定特征类型来单独调用,或者更好地通过像decode("component-n", Map)这样的字符串参数来调用。

不可能使用具有相同名称的方法,因为它们会因阴影或覆盖而丢失。我可以想到一个解决方案,其中所有方法都存储在Map[String, Map[String, String] => Unit]用于解码,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}
  }
}

最后

class EntityA extends ComponentA with ComponentB with ComponentC with ComponentD

这样

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)
  }
}

给出:

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)

A和B组件不会受到继承解析的影响。因此,此方法仅适用于某些层次结构案例。在这种情况下,我们看到ComponentD已经遮蔽了其他一切。欢迎提出任何意见。

更新2:我在这里放置了回答这个问题的评论,以便更好地参考:“Scala线性化所有特征。应该有一个超级的东西将终止链。在你的情况下,这意味着CA仍然应该调用super,而Component应该是使用no-op终止链的人。“ - Rex Kerr

1 个答案:

答案 0 :(得分: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"
}

有效吗?

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)

事实上!它不仅有效,而且您可以免费获得订单。

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

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 }
  }
}

这也像人们希望的那样有效:

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