Scala之自身類型(Self Type)與蛋糕模式(Cake Pattern)

目錄

本文基於Gregor Heine分享的PPT《Scala Self-Types》註解式地介紹自身引用(Self Type)和蛋糕模式(Cake Pattern),原PPT解釋地非常好,感興趣的朋友可以自行下載閱讀。本文原文出處: http://blog.csdn.net/bluishglc/article/details/60739183 轉載請註明出處。

設計一輛車

一輛汽車往往會包含這樣一些組件:

  • Engine
  • Fuel Tank
  • Wheels
  • Gearbox
  • Steering
  • Accelerator
  • Clutch
  • etc…

所以我們的Car類會包含上述的組件,爲了簡單起見,我們暫先只考慮Engine這一個組件,其他的組件可以此類推。

第一版的實現:基於繼承

trait Engine {
    private var running = false
    def start(): Unit = {
    if (!running) println("Engine started")
        running = true
    }
    def stop(): Unit = {
        if (running) println("Engine stopped")
        running = false
    }
    def isRunning(): Boolean = running
    def fuelType: FuelType
}

trait DieselEngine extends Engine {
    override val fuelType = FuelType.Diesel
}

trait Car extends Engine {
    def drive() {
        start()
        println("Vroom vroom")
    }
    def park() {
        if (isRunning() ) println("Break!")
        stop()
    }
}

val myCar = new Car extends DieselEngine

這一版的實現問題很明顯:myCar即是一輛車又是一臺發動機,這太怪異了,顯然,我們濫用了繼承。

第二版的實現:基於組合

trait Car {
    def engine : Engine
    def drive() {
        engine.start()
        println("Vroom vroom")
    }
    def park() {
        if (engine.isRunning() ) println("Break!")
        engine.stop()
    }
}

val myCar = new Car {
    override val engine = new DieselEngine()
}

這一版好了很多,實際上作爲一段普通的程序已經無可指摘,但是還是有“傲嬌”的同學會站出來說:要是能把engine通過依賴注入的方式傳給myCar就更好了。那好,我們繼續演進我們的代碼,但是接下來要先解釋一個自身類型(Self Type)

引入“自身類型”(Self Type)

《Programming in Scala》一書對“自身類型”(Self Type)的解釋是:
“A self type of a trait is the assumed type of this,the receiver, to be used within the trait. Any concrete class that mixes in the trait must ensure that its type conforms to the trait’s self type.”

一個特質的“自身類型”是這個特質要求的“this”指針(引用)的“實際”類型,它會作爲聲明的“那個類型”的實例在這個特質中使用,從而可以讓這個特質輕鬆地調用“那個類型”的字段和方法。所以,任何混入這個特質的具體類必須要同時保證它還是這個特質“自身類型”聲明的“那個類型”。感覺很繞口吧,還是看看例子吧:

trait Car {
    this: Engine => // self-type
    def drive() {
        start()
        println("Vroom vroom")
    }
    def park() {
        println("Break!")
        stop()
    }
}

val myCar = new Car extends DieselEngine

this:Engine =>就是我們說的自身類型聲明,這告訴編譯器,所有繼承Car的具體類必須同時也是一個Engine,因爲Car的業務代碼裏已經把this當成一個Engine在使用使用了(調用了它的startstop方法),如果具體類無法滿足Car的這一要求,編譯就將失敗。

對於前面第二版基於組合的實現,我們看到至少Car不再顯式地要求依賴一個Engine實例了,同時又不需要繼承一個Engine,這確實是一個進步,但是myCar的實例化依然是一個尷尬的存在,它即是Car又是
DieselEngine。看上去,自身類型(Self Type)爲我們打開了一扇門,但是還沒有完全解決我們的問題,那就來點“模式”吧。

引入“蛋糕模式”(Cake Pattern)

簡單來說,蛋糕模式的思路是:假如A依賴B,我們用一個特質把被依賴方B包裹起來,我們可以叫它BComp,再用一個特質把依賴A方包裹起來,我們可以叫它AComp,我們會把AComp的自身類型聲明爲BComp, 這樣我們可以在AComp中自由引用BComp的所有成員,這樣從形式上就實現了把B注入到A的效果。此外,兩個Comp都要有一個抽象的val字段來代表將來被實例化出來的A或B。最後就是粘合各個組件,這需要第三個類,它同時繼承Acomp和Bcomp,然後重寫Comp裏要求的val字段(在Scala裏除了重寫方法,還可以重寫字段),來實例化A和B,這樣,一切就都粘合併實例化好了。回到我們的例子,看看第三版的實現吧,基於蛋糕模式的標準實現:

trait EngineComponent {

    trait Engine {
        private var running = false
        def start(): Unit = { /* as before */ }
        def stop(): Unit = {/* as before */ }
        def isRunning: Boolean = running
        def fuelType: FuelType
    }

    protected val engine : Engine

    protected class DieselEngine extends Engine {
        override val fuelType = FuelType.Diesel
    }

}

trait CarComponent {

    this: EngineComponent => // gives access to engine

    trait Car {
        def drive(): Unit
        def park(): Unit
    }

    protected val car: Car

    protected class HondaCar extends Car {
        override def drive() {
            engine.start()
            println("Vroom vroom")
        }
        override def park() { … }
    }
}

//tie them all together
object App extends CarComponent with EngineComponent with FuelTankComponent with GearboxComponent {
    override protected val engine = new DieselEngine()
    override protected val fuelTank = new FuelTank(capacity = 60)
    override protected val gearBox = new FiveGearBox()
    override val car = new HondaCar()
}

App.car.drive()

App.car.park()

最後生產出的這個App.car是一輛Honda,柴油發動機,60升的油箱,5級變速。這一版實現繁雜了很多,但是有這樣幾個重點:
1. HondaCar在實現過程中使用到了Engine,但是它即沒有繼承Engine也沒實例化一個Engine字段,這和傳統的依賴注入在效果上是無差別的,實際上就是實現了把Engine注入到HondaCar的目標。
2. 粘合互相依賴的組件的過程發生在App的定義中。所有的組件都預留了protected val的字段,留待組裝粘合的時候實例化。
3. 主動要去依賴其他組件的組件必定要將依賴的組件聲明成自身類型,以便在組件內部自由引用被依賴組件的成員和方法。

利弊得失

蛋糕模式完全依賴語言自身的特性,沒有外部框架依賴,類型安全,可以獲得編譯期的檢查。但缺點也是很明顯的,代碼複雜,配置不靈活。就個人而言,不太會選擇使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章