保留特征个性,同时将它们混合在一起

7

我希望创建一个实体系统,其中包含一些特殊属性,基于Scala特性。

主要的想法是:所有组件都是继承自公共特性的特性:

trait Component
trait ComponentA extends Component

有时候,如果存在更复杂的层次结构和相互依赖的组件,情况可能会变成这样:
trait ComponentN extends ComponentM {
  self: ComponentX with ComponentY =>

  var a = 1
  var b = "hello"
}

等等。我得出的结论是,与每个组件相关的数据应该包含在组件本身中,而不是在Entity或其他地方的存储器内,因为访问速度更快。顺便说一句,这也是为什么一切都是可变的,所以不需要考虑不可变性。

然后创建Entities,混合Traits:

class Entity

class EntityANXY extends ComponentA
  with ComponentN
  with ComponentX
  with ComponentY

这里一切都很好,但我有一个特殊的要求,不知道如何用代码实现。要求是这样的:
每个特性必须提供一种编码方法,使得可以以通用形式收集与特性相关的数据,例如以JSON或者 Map 的形式,如Map("a" -> "1", "b" -> "hello") ,并提供一种解码方法,将这样的映射转换回与特性相关的变量。此外: 1)所有混合的特性的编码和解码方法都由Entity的方法encode和decode(Map)在任意顺序下进行调用,2)应该可用于通过指定特性类型或更好地通过字符串参数(如decode("component-n", Map))单独调用。
不能使用具有相同名称的方法,因为它们会因重写而被覆盖。我可以想到一种解决方案,即将所有方法存储在Map[String, Map[String, String] => Unit]中进行解码,以及在每个实体中将其储存在Map[String, () => Map[String, String]]中进行编码。这将起作用 - 命名和 bunch 调用肯定会可用。然而,这将导致在每个实体中存储相同的信息,这是不可接受的。
还可以将这些映射存储在伴随对象中,以便不会在任何地方重复,并使用额外的参数调用对象的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应该是终止链条的那个没有操作的部分。” - Rex Kerr

为什么不使用某种通用解决方案来实现编码,而是手动编写所有的编码方法呢? - Robin Green
@RobinGreen,你能展示这种解决方案的例子吗?我几乎确定它涉及更多的反射或更多的自动转换(尽管是手工制作)。有很多好的通用解决方案,但它们都会牺牲速度。而且我不确定这是否能帮助我实现这个任务的主要目标——创建一个轻量级的处理个体编码解码的解决方案。不过,如果你知道任何有趣的例子,请分享一下,也许我错过了它们,它们会适合我的需求? - noncom
当混合两个不同的组件并且它们都定义了相同名称的变量时,例如 trait A extends Component { var name = "" }trait B extends Component { var name = "" },你不会遇到一堆问题吗?我不明白为什么你不使用更惯用的 Cake Pattern。这样,名称冲突将更少,并且更容易避免,是吗? - Emil L
@EmilH 在这种情况下,名称冲突并不是一个问题。就我所了解的而言,蛋糕模式非常适合模块化和抽象化,但我不明白它如何帮助实现我所描述的功能,比如调用entity.encode("spatial")entity.encode[Spatial]()等方法来获取与特征“Spatial”相关的值的映射。如果您能向我解释如何使用蛋糕模式实现这一点,或者给我一个可以阅读的链接,我会很高兴的。 - noncom
1
如果名称冲突不是问题,那么我想就不需要 Cake 模式了(看到我的评论,我发现措辞有些强硬,抱歉 =))。要进行编码部分,您可以让所有组件都依赖于一个 trait(例如命名为 EncoderMapper),其中包含字符串(例如“空间”)和定义该编码应如何完成的函数之间的映射。然后,每个组件都可以向该映射添加条目。在实体上调用编码时,它可以简单地查找正确的编码器来完成工作。 - Emil L
@EmilH 一切都很好,我们总是希望以智慧和理解的名义分享知识 :))) 我认为 EncoderMapper 几乎与我建议的第一种方法相同 - 它将使用一个映射(对于一个实体类型的实例来说是相同的)的方法来加载每个实例?或者我理解得不太对吗?我不是 Scala 专家,所以我更多的是问题而不是答案 :) - noncom
1个回答

5
Travis的回答基本上是正确的,不确定为什么他删除了它。但是,无论如何,只要你愿意让你的编码方法接受一个额外的参数,并且在解码时你愿意设置可变变量而不是创建一个新对象,你就可以轻松地做到这一点。(复杂的运行时特性堆叠从难到不可能。)
基本观察是当你将trait链接在一起时,它定义了一个超类调用层次结构。如果每个调用都处理该trait中的数据,只要你能找到一种获取所有数据的方法,你就可以完成任务。所以
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

1
我删除了答案,因为在这种情况下整个“abstract override”业务实际上并没有起到任何作用(这就是早上回答问题的后果)。不管怎样,你的更好。 - Travis Brown
有趣的方法!我从未想过那种方式……确实,这是一种相当简洁且比较便宜的方式来完成此操作。但是为了扩展理论——你认为,在非线性父系层次结构的情况下会发生什么?例如,如果某个特征具有另一个特征作为其父级,则会发生什么?这确实是一个有趣的情况,看看我是否无法用这种继承模型得到所需的内容(我认为是这样)。让我们进一步探讨,请查看原问题的更新。 - noncom
2
@noncom - Scala 线性化 所有的 traits。每个 trait 都应该有一个超级 trait,用于终止链条。在你的情况下,这意味着 C 和 A 仍然需要调用 super,并且 Component 应该是用于以 no-op 终止链条的那个 trait。 - Rex Kerr
我明白了!感谢您的解释。这就是一个完整的解决方案!我很快会在实际应用中尝试它。 - noncom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接