當函式成為一等公民時,設計模式的變化

張_逸發表於2017-04-28

GOF提出的設計模式,其本質思想是封裝變化。故而,建立型模式封裝的是物件建立的變化,結構型模式封裝的是物件之間的協作與組合結構,行為型模式則封裝了物件行為的變化。所謂“行為”,不正是函式所能要表達的嗎?在支援FP的程式語言中,函式成為了一等公民,即它可以脫離物件而單獨存在,也能夠當做引數或返回值(高階函式)。於是,傳統的OO設計模式開始了變化,尤其是行為型模式。

函式的抽象能力

從函式的抽象角度看,任何行為都可以理解為是一個對型別進行轉換的函式,這是FP思想對OO設計模式的最大沖擊。例如Strategy模式與Command模式,前者封裝了演算法策略的變化,後者則封裝了命令請求的變化。無論演算法策略,還是命令請求,都可以表現為一個函式。

譬如說將各種四則運算看做是一種演算法策略,為了應對具體計算的變化,在Java中我們應該定義四則運算的策略介面:

public interface Strategy {  
    int compute(int a, int b);  
} 
public class Context  {  
    private final Strategy strategy;  
    public Context(Strategy strategy) { 
        this.strategy = strategy; 
    }  
    public void use(int a, int b) { 
        strategy.compute(a, b); 
    }  
}複製程式碼

接收兩個整數,然後經過計算返回一個整數。顯然,四則運算的呼叫者其實關注的不是Strategy這個介面,而是compute這個行為。跟進一步,呼叫者其實關注的是將兩個整數轉換為一個整數的行為,他並不關心介面是什麼,函式名有是什麼,而是關注f(a, b) = c這個函式。於是,在Scala中,策略模式的實現就變為:

class Context(f: (Int, Int) => Int) {  
  def use(a: Int, b: Int): Int =  f(a, b)  
}複製程式碼

當然,你可以可以為這個函式定義一個型別,使其更加表意:

type Stategy = (Int, Int) => Int複製程式碼

當然,如果面對的是一組策略行為的封裝,且這些策略行為的變化是一致的,使用一個介面將這些行為封裝起來,在重用和表意角度講,似乎又比單純使用函式更佳。Scala提供OO與FP兩種正規化,算是一種騎牆的取巧,程式設計師需要依勢而為。Scala給你提供了豐富而精彩的食材,如果你沒有將菜做得色香味俱全,不能怪食材不好,還是自己太爛了。

Scala還提供了一種類似block的機制,稱之為by name call。它接受的是一個語句塊,而非函式型別。所以要注意這種形式與無參函式的區別。此外,by name call同時還具有延遲呼叫的能力。例如,當我們定義一個invoke函式接受一個無傳入引數的函式時:

def invoke(f: () => Unit) = f()複製程式碼

如果你向invoke傳入println("scala"),scala會報告錯誤:

當函式成為一等公民時,設計模式的變化

這是因為println("scala")返回的是Unit型別,而不是() => Unit函式型別。使用by name call就沒有這個問題:

當函式成為一等公民時,設計模式的變化

f: => Unit是一個語句塊,所以不能像函式那樣呼叫。我們可以使用這種方式來快捷實現Command模式。

由於Java 8已經支援Lambda表示式,雖然它仍然不支援高階函式,但是作為Java程式設計師,仍然有必要培養函式抽象的能力與習慣。在Java 8中使用Lambda,不僅讓語法變得簡潔,還可以讓呼叫者可以脫離對具體某個介面的依賴,而僅僅依賴函式的抽象特徵。

函式的組合能力

FP的程式設計思想中,除了高階函式(包括Curry等)具有的抽象能力之外,還有一個好處是提供組合子能力。落實到Scala的語法上,就是偏函式(Partial Function)的andThencomposeorElse

Pavel Fatin在文章《Design Patterns in Scala》用OO設計模式中的Chain of Responsibility(職責鏈)模式來對比組合子,其實還是比較牽強的。或者說,FP思想中的組合子遠遠比職責鏈模式更強大。在Elixir語言中,甚至還提供了管道操作符|>實現這種函式的組合。而我在部落格《Scala中的Partial Function》中已經非常詳解地介紹了Scala的偏函式,大家可以移步閱讀。

如果真要對比,那麼結合Scala的語法來看,則orElse可以非常方便地模擬職責鏈模式,而andThen則近似於管道-過濾器模式。其實我在OO語言中,很少運用GOF標誌的職責鏈模式,也就是當尋找到具體職責的承擔者時,履行職責後即可退出的方式;而是對這種模式進行調整,讓其在履行職責後繼續執行next的職責,又近乎於管道-過濾器了。

所以說,設計模式的運用妙乎於心,講究應勢而變。在融入FP思想後,要從本質思想去面對這些模式,不拘泥於OO還是FP,似乎才是未來程式設計的取捨之道。

相關文章