基於 Scala Trait 的設計模式

在《做爲Scala語法糖的設計模式》博文中,我重點介紹了那些已經融入Scala語法的設計模式。今天要介紹的兩個模式,則主要與Scala的trait有關。html

Decorator Pattern

在GoF 23種設計模式中,Decorator Pattern算是一個比較特殊的模式。它充分利用了繼承和組合(或者委派)各自的優點,將它們混合起來,不只讓優點擴大,還讓各自的缺點獲得了抵消。Decorator模式的核心思想實際上是「職責分離」,即將要裝飾的職責與裝飾的職責分離,從而使得它們能夠在各自的繼承體系下獨立演化,而後經過傳遞對象(組合)的形式完成對被裝飾職責的重用java

從某種角度來說,裝飾職責與被裝飾職責之間的分離與各自抽象,不妨能夠看作是Bridge模式的變種。但不一樣之處在於Decorator模式又額外地引入了繼承,但不是爲了重用,而是爲了多態,使得裝飾者由於繼承自被裝飾者,從而擁有了被裝飾的能力。因此說,繼承的引入真真算得上是點睛之筆了。程序員

理解Decorator模式,必定要理解繼承與組合各自扮演的角色。簡而言之,就是:設計模式

  • 繼承:裝飾者的多態
  • 組合:被裝飾者的重用

正由於此,在Java代碼中實現Decorator模式,要注意裝飾器類在重寫被裝飾器的業務行爲時,必定要經過傳入的對象來調用被裝飾者的行爲。假設傳入的被裝飾者對象爲decoratee,則調用時就必定是decoratee,而不是super(因爲繼承的關係,裝飾類是能夠訪問super的)。框架

例如BufferedOutputStream類做爲裝飾類,要裝飾OutputStream的write行爲,就必須這樣實現:ide

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) {
        //這裏應該是調用decoratee, 而非super,雖然你能夠訪問super 
        decoratee.write(buffer)
    }
}複製代碼

然而,在Scala中實現Decorator模式,狀況卻有些不一樣了。Scala的trait既體現了Java Interface的語義,卻又能夠提供實現邏輯(至關於Java 8的default interface),並在編譯時採用mixin方式完成代碼的重用。換言之,trait已經完美地融合了繼承與組合的各自優點。所以,在Scala中若要實現Decorator模式,只須要定義trait去實現裝飾者的功能便可:post

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的定義中,根本看不到組合的影子,且在對write方法進行重寫時,調用的是super,這與我前面講到的內容背道而馳啊!ui

區別在於組合(delegation)的時機。在Java(原諒我,由於使用Scala的緣故,我對Java 8的default interface沒有研究,不知道是否與scala的trait徹底相同)語言中,組合是經過傳遞對象方式完成的職責委派與重用,也就是說,組合是在運行時發生的。Scala的實現則否則,在trait中利用abstract override關鍵字來完成一種stackable modifications,這種方式被稱之爲Stackable Trait Pattern。這種語法僅能用於trait,它表示trait會將某個具體類針對該方法提供的實現混入(mixin)到trait中。裝飾的客戶端代碼以下:this

new FileOutputStream("foo.txt") with Buffering複製代碼

FileOutputStream的write方法實如今編譯時就被混入到Buffering中。因此能夠稱這種組合爲靜態組合。spa

Dependency Injection

Dependency Injection(依賴注入或者稱爲IoC,即控制反轉)其實應該與依賴倒置原則結合起來理解,首先應該保證不依賴於實現細節,而是依賴於抽象(接口),而後,再考慮將具體依賴從類的內部轉移到外面,並在運行時將依賴注入到類的內部。這也是Dependency Injection的得名由來。

在Java世界,多數狀況下咱們會引入框架如Spring、Guice來完成依賴注入(這並非說依賴注入必定須要框架,嚴格意義上,只要將依賴轉移到外面,而後經過set或者構造器注入依賴,均可以認爲是實現了依賴注入),不管是基於xml配置,仍是annotation,或者Groovy,核心思想都是將對象之間的依賴設置(裝配)轉交給框架來完成。Scala也有相似的IoC框架。可是,多數狀況下,Scala程序員會充分利用trait與self type來實現所謂的依賴注入。這種設計模式在Scala中經常被暱稱爲Cake Pattern

一個典型的案例就是將一個Repository的實現注入到Service中。在Scala中,就應該將Repository的抽象定義爲trait,而後在具體的Service實現中,經過Self Type引入Repository:

trait Repository {
  def save(user: User)
}
trait DatabaseRepository extends Repository { /* ... */ }
trait UserService { 
  self: Repository => 
  def create(user: User) {
    //這裏調用的是Repository的save方法
    //調用Self Type的方法就像調用本身的方法通常
    save(user)
  }
}

//這裏的with完成了對DatabaseRepository依賴的注入
new UserService with DatabaseRepository複製代碼

Cake Pattern遵循了Dependency Inject的要求,只是它沒有像Spring或者Guice那樣完全將注入依賴的職責轉移給外部框架,而是將注入的權利交到了調用者手裏。這樣會致使調用端代碼並無徹底與具體依賴解耦,但在大多數狀況下,這種輕量級的依賴注入方式,反而更討人喜歡。

在Scala開發中,咱們經常會使用Cake Pattern。在個人一篇博客《一次設計演進之旅》中,就引入了Cake Pattern來完成將ReportMetadata依賴的注入。

說明:文中示例代碼主要來自Pavel Fatin的博客Design Patterns in Scala

相關文章
相關標籤/搜索