《編程絮語(yǔ)》之二
沒(méi)有對象協(xié)作的系統是不可想象的,因為此時(shí)的系統就是一個(gè)龐大的類(lèi),一個(gè)無(wú)所不知的“上帝類(lèi)”。每個(gè)對象都有自己的自治領(lǐng)域,“各人自?huà)唛T(mén)前雪”,對象定義的法則就是這么自私。單一職責原則(SRP)[1]體現的正是這樣的道理。對象的職責越少,則對象之間的依賴(lài)就越少。這一前提就是對象具有足夠的高內聚與細粒度。這樣的對象一方面有利于對象的重用,另一方面也保證了對象的穩定性。
對象的職責可以是自己承擔,也可以委派給其他對象。因此,有對象就必然有依賴(lài),正如有人就有江湖。那么,我們該如何降低對象之間的依賴(lài)?第一要則是依賴(lài)于抽象,如依賴(lài)倒置原則(DIP)[2]所云。如果無(wú)法依賴(lài)于抽象,則至少應該保證你所依賴(lài)的對象是足夠穩定的。事實(shí)上,最穩定的對象就是抽象對象,所以萬(wàn)法歸一,穩定才是降低依賴(lài)的基礎。
依賴(lài)之殤的源頭是“變化”。變化與穩定顯然是矛盾的,軟件設計的最大問(wèn)題就是如何協(xié)調這兩者之間的矛盾。我們需要像高明的雜技師,要學(xué)會(huì )掌握平衡,能夠在鋼絲繩上無(wú)礙的行走。那么,如何解決變化帶來(lái)的影響呢?答案是利用封裝來(lái)隔離變化。
封裝的一種方式是抽象,因為相對于實(shí)現而言,接口總能保持一定的穩定性。例如稅收策略。對于調用方而言,只是希望能夠得到準確的稅值,至于如何計算,則不是他關(guān)心的內容。抽象出計算稅值的接口,就能夠隔離調用方與可能變化的稅收策略之間的依賴(lài)關(guān)系,如下圖所示:

利用抽象還可以解除對特定實(shí)現環(huán)境例如外部資源、硬件或數據庫的依賴(lài)。此時(shí)抽象隔離的變化可能是外部環(huán)境提供的API。例如,在考勤系統中,利用抽象隔離不同型號考勤機的變化。

利用抽象解除對象之間的依賴(lài),還可以保證系統具有良好的可測試性。因為調用者依賴(lài)于抽象接口,就為我們引入Mock對象(當然也可以是Fake對象)執行單元測試提供了方便。尤其是當我們對領(lǐng)域對象進(jìn)行測試時(shí),如果領(lǐng)域對象需要對數據庫操作,可以通過(guò)依賴(lài)抽象的持久對象(或倉儲對象)實(shí)現職責的委派。此時(shí),我們可以引入持久對象的Mock對象,模擬領(lǐng)域對象持久化的職責,既分離了領(lǐng)域對象與數據庫資源的依賴(lài)關(guān)系,又能夠提高單元測試的效率。

void Add(OrderInfo order);
void Remove(OrderInfo order);
}
public class Order
{
private IOrderRepository m_repository;
public Order(IOrderRepository repository)
{
m_repository = repository;
}
public void Place(OrderInfo order)
{
if (order.Validate())
{
m_repository.Add(order);
}
}
}
利用封裝隔離變化,并非必須依賴(lài)于抽象,根據不同的場(chǎng)景,降低要求,依賴(lài)于較為穩定的具體類(lèi)對象也是可行的。這是一種降低復雜度的設計方式。例如,我們可以引入一個(gè)Helper類(lèi)來(lái)封裝第三方API的調用,從而實(shí)現調用方與第三方API的隔離。例如為SQLServer數據庫操作定義一個(gè)Helper類(lèi):
public static class SQLHelper
{
public int ExecuteNonQuery() {}
public DataSet ExecuteQuery() {}
}
這樣的設計類(lèi)似于Gateway模式[3],利用一個(gè)Gateway對象來(lái)封裝外部系統或資源訪(fǎng)問(wèn)。具體類(lèi)對象顯然不如抽象接口穩定,因此在設計時(shí),我們需要遵循單一職責原則。這樣的設計體現了DRY[4]原則,利用封裝避免代碼的重復,避免解決方案蔓延的壞味道[5]。合理的封裝可以將變化點(diǎn)集中或限制到一處,以應對變化。一個(gè)常見(jiàn)的例子是利用簡(jiǎn)單工廠(chǎng)模式,將所有對象的創(chuàng )建集中在一個(gè)類(lèi)中(當然也可以按模塊創(chuàng )建不同的靜態(tài)工廠(chǎng))。即使創(chuàng )建的產(chǎn)品對象發(fā)生了變化,我們也可以只修改靜態(tài)工廠(chǎng)類(lèi)一處的實(shí)現。簡(jiǎn)單工廠(chǎng)模式常??梢詰迷陬I(lǐng)域層中,通過(guò)工廠(chǎng)對象創(chuàng )建持久層對象(或所謂的數據訪(fǎng)問(wèn)對象)。
依賴(lài)源于對象的協(xié)作。傳遞依賴(lài)的方式可以通過(guò)屬性,構造函數或方法的參數。若要保證對象間的松散耦合,構造函數或方法的參數以及屬性的類(lèi)型就應定義為抽象類(lèi)型,如前面例子中的Order類(lèi)。這是依賴(lài)解耦的關(guān)鍵方式,完全符合“面向接口設計”的編程思想,同時(shí),它也有利于我們在后期實(shí)現“依賴(lài)注入”。
然而,產(chǎn)生依賴(lài)的方式絕不僅限于上述三種情形。例如,方法的返回值以及方法體中局部對象的創(chuàng )建,同樣可能產(chǎn)生依賴(lài)。比較而言,這種依賴(lài)關(guān)系更難解除,因為它與具體的實(shí)現緊密相關(guān)。換句話(huà)說(shuō),因為這兩種情形的依賴(lài)都涉及到具體對象的創(chuàng )建,且由實(shí)現者完成,而不能轉交給調用方。例如,在如下的設計中,消息頭會(huì )決定消息編碼的方式。

MessageHeader的GetEncoder()方法需要返回一個(gè)IEncoder對象,這就要求在方法體中創(chuàng )建一個(gè)具體的IEncoder對象。要解除這樣的依賴(lài)關(guān)系非常困難,如需徹底解除,一種可能是利用反射技術(shù),通過(guò)具體類(lèi)的類(lèi)型來(lái)創(chuàng )建。還有一種可能是利用“慣例優(yōu)于配置”實(shí)現解耦[6]。如果不需要徹底解除依賴(lài),也可以利用“表驅動(dòng)法”,或者直接將條件分支語(yǔ)句封裝到方法中。
如果在方法實(shí)現中需要創(chuàng )建一個(gè)局部對象,我們可以考慮簡(jiǎn)單工廠(chǎng)模式或Registry模式[3]。例如,在Role對象的IsAuthorized()方法中,需要創(chuàng )建一個(gè)PriviledgeFinder對象,通過(guò)調用它的FindPriviledges()方法獲得角色對應的權限集。此時(shí),我們可以在Registry對象中提供PriviledgeFinder對象:
interface IPriviledgeFinder
{
IList<Priviledge> GetPriviledges(int roleID);
}
public class PriviledgeFinder:IPriviledgeFinder
{}
public class Registry
{
private Registry()
{ }
private static Registry Instance = new Registry();
protected virtual IPriviledgeFinder m_priviledge = new PriviledgeFinder();
public static IPriviledgeFinder PriviledgeFinder()
{
return Instance.m_priviledge;
}
}
public class Role
{
public bool IsAuthorized()
{
IList<Priviledge> priviledges =
Registry.PriviledgeFinder().GetPriviledges(this.ID);
}
}
上述實(shí)現實(shí)際上仍然利用了“將變化集中在一處”的設計原則。注意Registry類(lèi)中的m_priviledge屬性是virtual的受保護屬性,它提供了一種變化的可能,可以交由子類(lèi)去實(shí)現。
如何知道一個(gè)類(lèi)是否過(guò)多的依賴(lài)其他類(lèi)?一個(gè)辦法就是創(chuàng )建這個(gè)類(lèi),并保證創(chuàng )建的對象能夠正常使用。如果創(chuàng )建的過(guò)程非常復雜,就說(shuō)明該類(lèi)的依賴(lài)過(guò)多。此時(shí),可以考慮分解該類(lèi)的職責。如果這些依賴(lài)是必須的,則可以考慮利用封裝,例如將對外部對象的調用修改為在內部創(chuàng )建(應用builder模式);也可以考慮使用Factory Method模式或者利用簡(jiǎn)單工廠(chǎng)。
依賴(lài)關(guān)系不僅僅只限于類(lèi)與類(lèi)之間,包(組件、模塊、層)與包(組件、模塊、層)之間同樣存在依賴(lài)關(guān)系。良好的設計需要包之間保持松散耦合。大體上講,包之間的依賴(lài)解除與類(lèi)之間的依賴(lài)解除方式是一致的。即:要求一個(gè)包盡量依賴(lài)于一個(gè)穩定的包。注意,一個(gè)包依賴(lài)于另一個(gè)包,就代表著(zhù)它依賴(lài)于這個(gè)包的每一個(gè)類(lèi)。Robert C.Martin說(shuō):“我放入一個(gè)包中的所有類(lèi)是不可分開(kāi)的,僅僅依賴(lài)于其中一部分的情況是不可能的。”[7]因此,我們可以將一個(gè)包看做是一個(gè)類(lèi),它仍然要求職責的高內聚。在包中對類(lèi)的分配,就相當于是對類(lèi)進(jìn)行一次分類(lèi)。共同封閉原則[7]要求:“包中的所有類(lèi)對于同一類(lèi)性質(zhì)的變化應該是共同封閉的。一個(gè)變化若對一個(gè)包產(chǎn)生影響,則將對該包中的所有類(lèi)產(chǎn)生影響,而對于其他的包不造成任何影響。”簡(jiǎn)言之,我們在對包進(jìn)行設計時(shí),需要避免將不同的職責耦合在一個(gè)包中,它會(huì )造成變化點(diǎn)的擴散。
解除包之間依賴(lài)關(guān)系的一個(gè)重要方法仍然是抽象。使用SeperatedInterface模式[3],在一個(gè)包中定義接口,而在另一個(gè)與這個(gè)包分離的包中實(shí)現這個(gè)接口。例如在分層架構模式中,我們常常對數據訪(fǎng)問(wèn)層進(jìn)行抽象,使得業(yè)務(wù)邏輯層依賴(lài)于該抽象層,而不是它的低層模塊。

上圖的設計實(shí)際上是依賴(lài)倒置原則的體現。在項目開(kāi)發(fā)中,這種將抽象與實(shí)現分別放在不同的包中,是系統設計中常見(jiàn)的方式。這樣的設計也能夠更好地應用在分布式開(kāi)發(fā)場(chǎng)景中。
[1]單一職責原則(Single Responsibility Principle):就一個(gè)類(lèi)而言,應該只專(zhuān)注于做一件事和僅有一個(gè)引起變化的原因;
[2]依賴(lài)倒置原則(Dependency Inversion Principle):高層模塊不應該依賴(lài)于低層模塊,二者都應該依賴(lài)于抽象;抽象不應該依賴(lài)于細節,細節應該依賴(lài)于抽象;
[3]Martin Fowler, Patterns of Enterprise Application Architecture;
[4]DRY原則,即“不要重復你自己(Don't Repeat Yourself)”它要求“系統中的每項知識只應該在一個(gè)地方描述。”
[5]Joshua Kerievsky, Refactoring to Patterns;
[6]文章《解除具體依賴(lài)的技術(shù)》
[7]Robert C. Martin Agile Software Development:Principles,Patterns and Practices
聯(lián)系客服