我希望创建一个实体系统,其中包含一些特殊属性,基于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
trait A extends Component { var name = "" }
和trait B extends Component { var name = "" }
,你不会遇到一堆问题吗?我不明白为什么你不使用更惯用的 Cake Pattern。这样,名称冲突将更少,并且更容易避免,是吗? - Emil Lentity.encode("spatial")
或entity.encode[Spatial]()
等方法来获取与特征“Spatial”相关的值的映射。如果您能向我解释如何使用蛋糕模式实现这一点,或者给我一个可以阅读的链接,我会很高兴的。 - noncomEncoderMapper
),其中包含字符串(例如“空间”)和定义该编码应如何完成的函数之间的映射。然后,每个组件都可以向该映射添加条目。在实体上调用编码时,它可以简单地查找正确的编码器来完成工作。 - Emil LEncoderMapper
几乎与我建议的第一种方法相同 - 它将使用一个映射(对于一个实体类型的实例来说是相同的)的方法来加载每个实例?或者我理解得不太对吗?我不是 Scala 专家,所以我更多的是问题而不是答案 :) - noncom