7

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

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

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's 方法以任意顺序调用encodedecode(Map)并且 2)应该通过指定特征类型来单独调用,或者更好的是,通过字符串参数,如decode("component-n", Map).

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

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

这个要求可能看起来很奇怪,但由于所需的速度和模块化,它是必要的。所有这些解决方案都很笨拙,我认为 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线性化了所有特征。应该有一个超特征会终止链。在你的情况下,这意味着C并且A仍然应该调用super, 和Component应该是用无操作终止链的那个。” ——雷克斯·克尔

4

1 回答 1

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
于 2012-08-22T18:18:08.537 回答