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. 主动要去依赖其他组件的组件必定要将依赖的组件声明成自身类型,以便在组件内部自由引用被依赖组件的成员和方法。

利弊得失

蛋糕模式完全依赖语言自身的特性,没有外部框架依赖,类型安全,可以获得编译期的检查。但缺点也是很明显的,代码复杂,配置不灵活。就个人而言,不太会选择使用。

猜你喜欢

转载自blog.csdn.net/bluishglc/article/details/60739183