我想创建一个基于 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 方法以任意顺序调用encode
,decode(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
应该是用无操作终止链的那个。” ——雷克斯·克尔