你看那代碼,好像一條鏈哎
就如星爺多年前說的那樣“你看那代碼,好像一條鏈哎”。什么?他沒說過嗎,或許我記錯了。你應(yīng)該已經(jīng)猜到了,這篇文章,我們來討論一下責任鏈設(shè)計模式。這個模式并不流行,至少在 Gang of Four定義的模式中是這樣。但現(xiàn)代依賴注入框架讓我們可以用巧妙的新奇的方式去實現(xiàn)這個模式,我們來看看。
介紹
聲明:這種模式并沒有新東西。我的一個同事剛剛前幾天使用過,我也曾用過很多次。這篇文章的靈感來源于我最近遇到的問題,我們下面來說說,我之前也沒有意識到這個問題可以用這種模式來解決。
傳統(tǒng)模式
責任鏈模式是一種行為設(shè)計模式,它***在Gang of Four寫的Design Patterns這本書中提及。模式的目的是:
避免請求的發(fā)送者與接收者耦合,為多個對象提供處理請求的機會.將接收對象串聯(lián)成鏈,請求在鏈上傳遞,直到被一個對象處理.
類的關(guān)系圖如下所示:
通過定義一個可以用來響應(yīng)客戶端請求的標準接口,來實現(xiàn)松耦合。在上面的圖中,表現(xiàn)為Handler抽象類型。可以通過創(chuàng)建鏈式的類,繼承上面的接口來實現(xiàn)多個類響應(yīng)請求的能力。每一個類在鏈中擁有下一個節(jié)點的實例。successor屬性滿足作用域。
當調(diào)用時,每一個handler確定自己是否有能力處理請求。如果有,它執(zhí)行請求的操作,在這,我們可以根據(jù)請求的轉(zhuǎn)發(fā)規(guī)則實現(xiàn)許多不同的處理方式。一旦一個ConcreteHandler聲明可以處理這個請求,我們可以實現(xiàn)規(guī)則用于停止請求在鏈中傳遞。這種情況下,handleRequest方法的實現(xiàn)方式如下所示:
if (/* The request can be successfully handled */) {
// Handle the request
} else {
successor.handleRequest(request);
}
另一方面,我們可以將請求轉(zhuǎn)發(fā)到鏈中的下一個handler,無論當前的handler是否能處理。
if (/* The request can be successfully handled */) {
// Handle the request
}
successor.handleRequest(request);
構(gòu)建鏈的操作應(yīng)該和下面差不多。
Handler chain = new ConcreteHandler1(new ConcreteHandler2(new ConcreteHandler3()));
chain.handleRequest(request);
在JDK內(nèi)部實現(xiàn)中,至少有兩個地方用到了這種模式:
- logging機制的實現(xiàn):java.util.logging.Logger#log()
- http請求過濾器機制和Servlet響應(yīng)規(guī)范的實現(xiàn):javax.servlet.Filter#doFilter()
依賴注入的出現(xiàn)
正如許多其他的情況一樣,依賴注入模式的出現(xiàn)改變了一切。讓我們看看依賴注入特性如何使責任鏈模式現(xiàn)代化。
首先,我們需要一個所有依賴注入庫都實現(xiàn)的特性:multibindings?;旧?,它可以提供一個類型的所有子類型的實例,僅僅通過注入這個類型的集合。
比如下面這個類型系統(tǒng):
interface Shop {}
class PetShop implements Shop {}
class Grocery implements Shop {}
class Drugstore implements Shop {}
// And so on...
現(xiàn)在,我們定義一個新類型ShoppingCenter,它擁有Shop每個子類型的實例。使用依賴注入,我們可以通過在ShoppingCenter注入一個Shop集合來實現(xiàn)這一目標。
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,稍稍復(fù)雜,但最終結(jié)果一樣。
責任鏈模式的現(xiàn)代化實現(xiàn)
簡單總結(jié)一下:我們已經(jīng)看到了責任鏈模式的典型形式;我們看到了依賴注入庫提供的multibinding特性;***,我們看到了如何把這兩個概念搭配使用。
首先,我們需要一個與原始的責任鏈設(shè)計模式稍有不同的實現(xiàn)。讓我們引入一個新的類型ChainHandler。這個類型的職責就是擁有整個鏈,并暴露出一個接口,用于訪問鏈提供給客戶端的操作函數(shù)。
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));
}
}
利用依賴注入的優(yōu)勢,在不改變已有代碼的基礎(chǔ)上增加一個Handler的實現(xiàn)。這意味著實際上我們不需要執(zhí)行回歸測試。另一方面,將Handler的執(zhí)行放入鏈中有一點困難(但并不是不能)
警告
正如很多其他的模式一樣,專注于構(gòu)造模式的每個類的角色是什么很重要。你會給每個具體的Handler什么功能?你會把應(yīng)用的業(yè)務(wù)邏輯直接放在Handler里面嗎?
首先,我們很多人都會提供上面的解決方案,這并不完全錯誤。然而,這種設(shè)計限制了代碼的復(fù)用并違反了單一職責原則(Single Responsibility Principle)。
舉個例子,我們需要實現(xiàn)一個系統(tǒng),用來在金融業(yè)務(wù)中補全信息,補全操作使用責任鏈模式。一個可能要插入的補全信息就是根據(jù)IBAN(國際銀行賬號)或BIC碼(銀行代碼)導(dǎo)出的收款人國家。然后我們來定義一個CountryPayeeEnricher。
首先看一下,我們可以在CountryPayeeEnricher中直接編寫代碼用來補全國家信息。但如果我們需要在我們應(yīng)用的其他位置(或其他應(yīng)用)復(fù)用這個功能呢?遵循組合原則是一個更好地解決方案,將代碼放進一個專有的類中,比如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類型,提供可復(fù)用的直接聯(lián)系收款人信息的服務(wù)。CountryPayeeEnricher類型,代替之前類型提供服務(wù)的標準入口。
Scala方式
為了***,我也想討論一下用Scala語言實現(xiàn)責任鏈模式。正如很多其他設(shè)計模式一樣,這門語言內(nèi)部已經(jīng)實現(xiàn)了責任鏈模式:偏函數(shù)(partial functions)。在理論層面,偏函數(shù)是定義了域里的一部分值的函數(shù)。在Scala中,這種函數(shù)有一個特別的類型——PartialFunction[T, V]
在Scala中使用模式匹配(pattern matching)聲明來定義偏函數(shù),在下面這個例子中,fraction的默認值是0。
val fraction: PartialFunction[Int, Int] = {
case d: Int if d != 0 => 42 / d
}
如果有多個定義集合,你可以有多個case子句。如果你為了應(yīng)用函數(shù),把每個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"
//. ..
}
緊接著,一個偏函數(shù)可以當做好多handler構(gòu)成的鏈。顯然,通過這種方式使用責任鏈模式,你必須遵守一些額外的約束。事實上:
- 你不能在每個handler中儲存元數(shù)據(jù)
- 你不能從鏈中移除handler
- 你不能顯示檢查handler或美觀的打印它
如果你確實不需要做上面這些事情,模式匹配偏函數(shù)(pattern-matching PartialFunctions)用起來相當棒。