你看那程式碼,好像一條鏈哎

2017-01-11    分類:程式設計開發、設計模式、首頁精華0人評論發表於2017-01-11

本文由碼農網 – 孫騰浩原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

就如星爺多年前說的那樣“你看那程式碼,好像一條鏈哎”。什麼?他沒說過嗎,或許我記錯了。你應該已經猜到了,這篇文章,我們來討論一下責任鏈設計模式。這個模式並不流行,至少在 Gang of Four定義的模式中是這樣。但現代依賴注入框架讓我們可以用巧妙的新奇的方式去實現這個模式,我們來看看。

介紹

宣告:這種模式並沒有新東西。我的一個同事剛剛前幾天使用過,我也曾用過很多次。這篇文章的靈感來源於我最近遇到的問題,我們下面來說說,我之前也沒有意識到這個問題可以用這種模式來解決。

傳統模式

責任鏈模式是一種行為設計模式,它首次在Gang of Four寫的Design Patterns這本書中提及。模式的目的是:

避免請求的傳送者與接收者耦合,為多個物件提供處理請求的機會.將接收物件串聯成鏈,請求在鏈上傳遞,直到被一個物件處理.

類的關係圖如下所示:

通過定義一個可以用來響應客戶端請求的標準介面,來實現鬆耦合。在上面的圖中,表現為Handler抽象型別。可以通過建立鏈式的類,繼承上面的介面來實現多個類響應請求的能力。每一個類在鏈中擁有下一個節點的例項。successor屬性滿足作用域。

當呼叫時,每一個handler確定自己是否有能力處理請求。如果有,它執行請求的操作,在這,我們可以根據請求的轉發規則實現許多不同的處理方式。一旦一個ConcreteHandler宣告可以處理這個請求,我們可以實現規則用於停止請求在鏈中傳遞。這種情況下,handleRequest方法的實現方式如下所示:

if (/* The request can be successfully handled */) {
    // Handle the request
} else {
    successor.handleRequest(request);
}

另一方面,我們可以將請求轉發到鏈中的下一個handler,無論當前的handler是否能處理。

if (/* The request can be successfully handled */) {
    // Handle the request
}
successor.handleRequest(request);

構建鏈的操作應該和下面差不多。

Handler chain = new ConcreteHandler1(new ConcreteHandler2(new ConcreteHandler3()));
chain.handleRequest(request);

在JDK內部實現中,至少有兩個地方用到了這種模式:

依賴注入的出現

正如許多其他的情況一樣,依賴注入模式的出現改變了一切。讓我們看看依賴注入特性如何使責任鏈模式現代化。

首先,我們需要一個所有依賴注入庫都實現的特性:multibindings。基本上,它可以提供一個型別的所有子型別的例項,僅僅通過注入這個型別的集合。

比如下面這個型別系統:

interface Shop {}
class PetShop implements Shop {}
class Grocery implements Shop {}
class Drugstore implements Shop {}
// And so on...

現在,我們定義一個新型別ShoppingCenter,它擁有Shop每個子型別的例項。使用依賴注入,我們可以通過在ShoppingCenter注入一個Shop集合來實現這一目標。

class ShoppingCenter {
    private final Set<Shop> shops;
    @Inject
    public void ShoppingCenter(Set<Shop> shops) {
        this.shops = shops;
    }
    // Class body using shops
}

真TM簡單!顯然,每一個依賴注入庫都有自己的配置來解決這種情況。在Spring中,使用auto-discovery特性,你只需要一點小小的配置。在Guice,稍稍複雜,但最終結果一樣。

責任鏈模式的現代化實現

簡單總結一下:我們已經看到了責任鏈模式的典型形式;我們看到了依賴注入庫提供的multibinding特性;最後,我們看到了如何把這兩個概念搭配使用。

首先,我們需要一個與原始的責任鏈設計模式稍有不同的實現。讓我們引入一個新的型別ChainHandler。這個型別的職責就是擁有整個鏈,並暴露出一個介面,用於訪問鏈提供給客戶端的操作函式。

class ChainHandler {
    private final Set<Handler> handlers;
    @Inject
    public void ChainHandler(Set<Handler> handlers) {
        this.handlers = handlers;
    }
    // Pass the request to each handler of the chain
    public void handle(final Request request) {
        handlers.forEach(h -> h.handle(request));
    }
}

利用依賴注入的優勢,在不改變已有程式碼的基礎上增加一個Handler的實現。這意味著實際上我們不需要執行迴歸測試。另一方面,將Handler的執行放入鏈中有一點困難(但並不是不能)

警告

正如很多其他的模式一樣,專注於構造模式的每個類的角色是什麼很重要。你會給每個具體的Handler什麼功能?你會把應用的業務邏輯直接放在Handler裡面嗎?

首先,我們很多人都會提供上面的解決方案,這並不完全錯誤。然而,這種設計限制了程式碼的複用並違反了單一職責原則(Single Responsibility Principle)。

舉個例子,我們需要實現一個系統,用來在金融業務中補全資訊,補全操作使用責任鏈模式。一個可能要插入的補全資訊就是根據IBAN(國際銀行賬號)或BIC碼(銀行程式碼)匯出的收款人國家。然後我們來定義一個CountryPayeeEnricher。

首先看一下,我們可以在CountryPayeeEnricher中直接編寫程式碼用來補全國家資訊。但如果我們需要在我們應用的其他位置(或其他應用)複用這個功能呢?遵循組合原則是一個更好地解決方案,將程式碼放進一個專有的類中,比如PayeeService:

class PayeeService {
    public Country deriveCountryFromPayee(String payee) {
        // Code that extract the country information from the
        // input payess
    }
    // Omissis...
}
class CountryPayeeEnricher implements Enrichment {
    private PayeeService payeeService;
    @Inject
    public void CountryPayeeEnricher(PayeeService payeeService) {
        this.payeeService = payeeService;
    }
    public void handle(Transaction tx) {
        Country country = payeeService.deriveCountryFromPayee(tx.getPayee());
        tx.setCountry(country);
        // ...or something like this
    }
}

通過這種方式,我們最終有了兩個擁有不同職責的型別:PayeeService型別,提供可複用的直接聯絡收款人資訊的服務。CountryPayeeEnricher型別,代替之前型別提供服務的標準入口。

Scala方式

為了完美,我也想討論一下用Scala語言實現責任鏈模式。正如很多其他設計模式一樣,這門語言內部已經實現了責任鏈模式:偏函式(partial functions)。在理論層面,偏函式是定義了域裡的一部分值的函式。在Scala中,這種函式有一個特別的型別——PartialFunction[T, V]

在Scala中使用模式匹配(pattern matching)宣告來定義偏函式,在下面這個例子中,fraction的預設值是0。

val fraction: PartialFunction[Int, Int] = {
  case d: Int if d != 0 =>  42 / d
}

如果有多個定義集合,你可以有多個case子句。如果你為了應用函式,把每個case子句作為滿足的情況(責任鏈裡的handler,記得嗎?),你就再次用到了責任鏈:

case class Request(val value: String) { /* ... */ }
val someStupidFunction: PartialFunction[Request, String] = {
  case Request(42) => "The final answer"
  case Request(0) => "You know nothing, John Snow"
  case Request(666) => "Something strange is going on in here"
  //. ..
}

緊接著,一個偏函式可以當做好多handler構成的鏈。顯然,通過這種方式使用責任鏈模式,你必須遵守一些額外的約束。事實上:

  • 你不能在每個handler中儲存後設資料
  • 你不能從鏈中移除handler
  • 你不能顯示檢查handler或美觀的列印它

如果你確實不需要做上面這些事情,模式匹配偏函式(pattern-matching PartialFunctions)用起來相當棒。

譯文連結:http://www.codeceo.com/article/code-looks-like-a-chain.html
英文原文:Code Looks Like a Chain
翻譯作者:碼農網 – 孫騰浩
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章