基于Scala Trait的設(shè)計(jì)模式
在《作為Scala語(yǔ)法糖的設(shè)計(jì)模式》中,我重點(diǎn)介紹了那些已經(jīng)融入Scala語(yǔ)法的設(shè)計(jì)模式。今天要介紹的兩個(gè)設(shè)計(jì)模式,則主要與Scala的trait有關(guān)。
Decorator Pattern
在GoF 23種設(shè)計(jì)模式中,Decorator Pattern算是一個(gè)比較特殊的模式。它充分利用了繼承和組合(或者委派)各自的優(yōu)勢(shì),將它們混合起來(lái),不僅讓優(yōu)勢(shì)擴(kuò)大,還讓各自的缺點(diǎn)得到了抵消。Decorator模式的核心思想其實(shí)是“職責(zé)分離”,即將要裝飾的職責(zé)與裝飾的職責(zé)分離,從而使得它們可以在各自的繼承體系下獨(dú)立演化,然后通過(guò)傳遞對(duì)象(組合)的形式完成對(duì)被裝飾職責(zé)的重用。
從某種角度來(lái)講,裝飾職責(zé)與被裝飾職責(zé)之間的分離與各自抽象,不妨可以看做是Bridge模式的變種。但不同之處在于Decorator模式又額外地引入了繼承,但不是為了重用,而是為了多態(tài),使得裝飾者因?yàn)槔^承自被裝飾者,從而擁有了被裝飾的能力。所以說(shuō),繼承的引入真真算得上是點(diǎn)睛之筆了。
理解Decorator模式,一定要理解繼承與組合各自扮演的角色。簡(jiǎn)而言之,就是:
- 繼承:裝飾者的多態(tài)
- 組合:被裝飾者的重用
正因?yàn)榇?,在Java代碼中實(shí)現(xiàn)Decorator模式,要注意裝飾器類在重寫被裝飾器的業(yè)務(wù)行為時(shí),一定要通過(guò)傳入的對(duì)象來(lái)調(diào)用被裝飾者的行為。假設(shè)傳入的被裝飾者對(duì)象為decoratee,則調(diào)用時(shí)就一定是decoratee,而不是super(由于繼承的關(guān)系,裝飾類是可以訪問(wèn)super的)。
例如BufferedOutputStream類作為裝飾類,要裝飾OutputStream的write行為,就必須這樣實(shí)現(xiàn):
- public interface OutputStream {
- void write(byte b);
- void write(byte[] b);
- }
- public class FileOutputStream implements OutputStream { /* ... */ }
- public class BufferedOutputStream extends OutputStream {
- //這里是組合的被裝飾者
- protected final OutputStream decoratee;
- public BufferedOutputStream(OutputStream decoratee) {
- this.decoratee = decoratee;
- }
- public void write(byte b) {
- //這里應(yīng)該是調(diào)用decoratee, 而非super,雖然你可以訪問(wèn)super
- decoratee.write(buffer)
- }
- }
然而,在Scala中實(shí)現(xiàn)Decorator模式,情況卻有些不同了。Scala的trait既體現(xiàn)了Java Interface的語(yǔ)義,卻又可以提供實(shí)現(xiàn)邏輯(相當(dāng)于Java 8的default interface),并在編譯時(shí)采用mixin方式完成代碼的重用。換言之,trait已經(jīng)***地融合了繼承與組合的各自優(yōu)勢(shì)。因此,在Scala中若要實(shí)現(xiàn)Decorator模式,只需要定義trait去實(shí)現(xiàn)裝飾者的功能即可:
- trait OutputStream {
- def write(b: Byte)
- def write(b: Array[Byte])
- }
- class FileOutputStream(path: String) extends OutputStream { /* ... */ }
- trait Buffering extends OutputStream {
- abstract override def write(b: Byte) {
- // ...
- super.write(buffer)
- }
- }
在Buffering的定義中,根本看不到組合的影子,且在對(duì)write方法進(jìn)行重寫時(shí),調(diào)用的是super,這與我前面講到的內(nèi)容背道而馳啊!
區(qū)別在于組合(delegation)的時(shí)機(jī)。在Java(原諒我,因?yàn)槭褂肧cala的緣故,我對(duì)Java 8的default interface沒(méi)有研究,不知道是否與scala的trait完全相同)語(yǔ)言中,組合是通過(guò)傳遞對(duì)象方式完成的職責(zé)委派與重用,也就是說(shuō),組合是在運(yùn)行時(shí)發(fā)生的。Scala的實(shí)現(xiàn)則不然,在trait中利用abstract override關(guān)鍵字來(lái)完成一種stackable modifications,這種方式被稱之為Stackable Trait Pattern。這種語(yǔ)法僅能用于trait,它表示trait會(huì)將某個(gè)具體類針對(duì)該方法提供的實(shí)現(xiàn)混入(mixin)到trait中。裝飾的客戶端代碼如下:
- new FileOutputStream("foo.txt") with Buffering
FileOutputStream的write方法實(shí)現(xiàn)在編譯時(shí)就被混入到Buffering中。所以可以稱這種組合為靜態(tài)組合。
Dependency Injection
Dependency Injection(依賴注入或者稱為IoC,即控制反轉(zhuǎn))其實(shí)應(yīng)該與依賴倒置原則結(jié)合起來(lái)理解,首先應(yīng)該保證不依賴于實(shí)現(xiàn)細(xì)節(jié),而是依賴于抽象(接口),然后,再考慮將具體依賴從類的內(nèi)部轉(zhuǎn)移到外面,并在運(yùn)行時(shí)將依賴注入到類的內(nèi)部。這也是Dependency Injection的得名由來(lái)。
在Java世界,多數(shù)情況下我們會(huì)引入框架如Spring、Guice來(lái)完成依賴注入(這并不是說(shuō)依賴注入一定需要框架,嚴(yán)格意義上,只要將依賴轉(zhuǎn)移到外面,然后通過(guò)set或者構(gòu)造器注入依賴,都可以認(rèn)為是實(shí)現(xiàn)了依賴注入),無(wú)論是基于xml配置,還是annotation,或者Groovy,核心思想都是將對(duì)象之間的依賴設(shè)置(裝配)轉(zhuǎn)交給框架來(lái)完成。Scala也有類似的IoC框架。但是,多數(shù)情況下,Scala程序員會(huì)充分利用trait與self type來(lái)實(shí)現(xiàn)所謂的依賴注入。這種設(shè)計(jì)模式在Scala中常常被昵稱為Cake Pattern。
一個(gè)典型的案例就是將一個(gè)Repository的實(shí)現(xiàn)注入到Service中。在Scala中,就應(yīng)該將Repository的抽象定義為trait,然后在具體的Service實(shí)現(xiàn)中,通過(guò)Self Type引入Repository:
- trait Repository {
- def save(user: User)
- }
- trait DatabaseRepository extends Repository { /* ... */ }
- trait UserService {
- self: Repository =>
- def create(user: User) {
- //這里調(diào)用的是Repository的save方法
- //調(diào)用Self Type的方法就像調(diào)用自己的方法一般
- save(user)
- }
- }
- //這里的with完成了對(duì)DatabaseRepository依賴的注入
- new UserService with DatabaseRepository
Cake Pattern遵循了Dependency Inject的要求,只是它沒(méi)有像Spring或者Guice那樣徹底將注入依賴的職責(zé)轉(zhuǎn)移給外部框架,而是將注入的權(quán)利交到了調(diào)用者手里。這樣會(huì)導(dǎo)致調(diào)用端代碼并沒(méi)有完全與具體依賴解耦,但在大多數(shù)情況下,這種輕量級(jí)的依賴注入方式,反而更討人喜歡。
在Scala開(kāi)發(fā)中,我們常常會(huì)使用Cake Pattern。在我的一篇文章《一次設(shè)計(jì)演進(jìn)之旅》中,就引入了Cake Pattern來(lái)完成將ReportMetadata依賴的注入。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】