2012-08-22 66 views
7

我想創建一個基於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("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中有更好的和慣用的解決方案,或者我錯過了一些重要的架構模式。那麼是否有比伴隨對象更簡單和更習慣的方法?

編輯:我認爲聚合而不是繼承可能可以解決這些問題,但代價是無法直接在實體上調用方法。

更新:探索雷克斯克爾提出的相當有希望的方式,我偶然發現了一些阻礙。下面是測試例:

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的線性化所有的特質應該是一切都將終止鏈的supertrait在你的情況,這意味着CA仍然應該叫super,並且Component應該是一個終止與無操作鏈。「 - 雷克斯克爾

+0

你爲什麼不使用某種通用的解決方案,以實現自己的編碼,而不是手工編寫所有的編碼方法? –

+0

@RobinGreen你能舉一個這樣的解決方案的例子嗎?我幾乎可以肯定的是,它涉及到比手工製作更多的反射或更多的額外投射(儘管是自動的)。有很多很好的通用解決方案,但它們都花費在速度上。此外,我不確定這是否能幫助我實現此任務的主要目標 - 爲處理單個編碼解碼創建一個超薄解決方案。雖然如果你知道任何有趣的例子,請分享,也許我錯過了他們,他們會適合? – noncom

+0

混合兩個不同的組件時,不會遇到一系列問題,這兩個組件都定義了一個具有相同名稱的變量,例如'特質A擴展組件{var name =「」}'和'特質B擴展組件{var name =「」}'?我不明白你爲什麼不使用更習慣的蛋糕模式。這種名稱衝突會更罕見,更容易避免是嗎? –

回答

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 = [email protected] 

scala> a.encode 
res8: Seq[String] = List(A, B) 

scala> val b = new B with A 
b: java.lang.Object with B with A = [email protected] 

scala> b.encode 
res9: Seq[String] = List(B, A) 

確實!它不僅可以工作,而且可以免費獲得訂單。

現在我們需要一種基於此編碼設置變量的方法。在這裏,我們遵循相同的模式 - 我們採取一些輸入,並與它一起走上超級鏈。如果你有很多特徵疊加在上面,你可能想要將文本預解析成一張圖,或者過濾出適用於當前特徵的那些部分。如果沒有,只需將所有內容傳遞給super,然後設置好自己。

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 = [email protected] 

scala> val d = new D with C 
d: java.lang.Object with D with C = [email protected] 

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

我刪除了答案,因爲整個「抽象覆蓋」業務實際上並沒有做任何事情在這種情況下(我得到回答問題的第一件事早上)。無論如何,你的情況會更好。 –

+0

一個有趣的方法!我從來沒有想過這種方式......真的,它提供了一個相當簡潔和相對便宜的方式來做到這一點。但是,要擴大理論 - 你如何看待,在一個非線性父母層次結構的情況下會發生什麼?例如,如果有一個特徵具有另一個特徵,那麼它又是什麼?這確實是一個有趣的案例,看看我無法得到我需要的繼承模型(我認爲)。讓我們進一步探討,請參閱原始問題的更新。 – noncom

+2

@noncom - Scala_線性化所有特徵。應該有一切將終止鏈條的超級特性。在你的情況下,這意味着C和A仍然應該調用super,並且'Component'應該是用no-op來終止鏈的那個。 –

相關問題