關於開放封閉原則,其核心的思想是:
軟體實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。
因此,開放封閉原則主要體現在兩個方面:
對擴展開放,意味著有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。
對修改封閉,意味著類一旦設計完成,就可以獨立完成其工作,而不要對類進行任何修改。
“需求總是變化”、“世界上沒有一個軟體是不變的”,這些言論是對軟體需求最經典的表白。從中透射出一個關鍵的意思就是,對於軟體設計者來說,必須在不需要對原有的系統進行修改的情況下,實現靈活的系統擴展。而如何能做到這一點呢?
只有依賴於抽象。實現開放封閉的核心思想就是對抽象編程,而不對具體編程,因為抽象相對穩定。讓類依賴於固定的抽象,所以對修改就是封閉的;而通過面向對象的繼承和對多態機制,可以實現對抽象體的繼承,通過覆寫其方法來改變固有行為,實現新的擴展方法,所以對於擴展就是開放的。這是實施開放封閉原則的基本思路,同時這種機制是建立在兩個基本的設計原則的基礎上,這就是Liskov替換原則和合成/聚合復用原則。關於這兩個原則,我們在本書的其他部分都有相應的論述,在套用反思部分將有深入的討論。
對於違反這一原則的類,必須進行重構來改善,常用於實現的設計模式主要有Template Method模式和Strategy模式。而封裝變化,是實現這一原則的重要手段,將經常發生變化的狀態封裝為一個類。
套用反思
站在銀行視窗焦急等待的用戶,在長長的隊伍面前顯得無奈。所以,將這種無奈遷怒到銀行的頭上是理所當然的,因為銀行業務的管理顯然有不當之處。銀行的業務人員面對蜂擁而至的客戶需求,在排隊等待的人們並非只有一種需求,有人存款、有人轉賬,也有人申購基金,繁忙的業務員來回在不同的需求中穿梭,手忙腳亂的尋找各種處理單據,電腦系統的功能模組也在不同的需求要求下來回切換,這就是一個發生在銀行視窗內外的無奈場景。而我每次面對統一排隊的叫號系統時,都為前面長長的等待人群而叫苦,從梳理銀行業務員的職責來看,在管理上他們負責的業務過於繁多,將其對應為軟體設計來實現,你可以將這種拙劣的設計表示如圖1所示。
按照上述設計的思路,銀行業務員要處理的工作,是以這種方式被實現的:
class BusyBankStaff
{
private BankProcess bankProc = new BankProcess();
// 定義銀行員工的業務操作
public void HandleProcess(Client client)
{
switch (client.ClientType)
{
case "存款用戶":
bankProc.Deposit();
break;
case "轉賬用戶":
bankProc.Transfer();
break;
case "取款戶":
bankProc.DrawMoney();
break;
}
}
}
這種設計和實際中的銀行業務及其相似,每個BusyBankStaff(“繁忙的”業務員)接受不同的客戶要求,一陣手忙腳亂的選擇處理不同的操作流程,就像示例代碼中的實現的Switch規則,這種被動式的選擇造成了大量的時間浪費,而且容易在不同的流程中發生錯誤。同時,更為嚴重的是,再有新的業務增加時,你必須修改BankProcess中的業務方法,同時修改Switch增加新的業務,這種方式顯然破壞了原有的格局,以設計原則的術語來說就是:對修改是開放的。 以這種設計來應對不斷變化的銀行業務,工作人員只能變成BusyBankStaff了。分析這種僵化的代碼,至少有以下幾點值得關註:銀行業務封裝在一個類中,違反單一職責原則;有新的業務需求發生時,必須通過修改現有代碼來實現,違反了開放封閉原則。
解決上述麻煩的唯一辦法是套用開放封閉原則:對擴展開放,對修改封閉。我們回到銀行業務上看:為什麼這些業務不能做以適應的調整呢?每個業務員不必周鏇在各種業務選項中,將存款、取款、轉賬、外匯等不同的業務分視窗進行,每個業務員快樂地專注於一件或幾件相關業務,就會輕鬆許多。綜合套用單一職責原則來梳理銀行業務處理流程,將職責進行有效的分離;而這樣仍然沒有解決業務自動處理的問題,你還是可以聞到僵化的壞味道在系統中瀰漫。
套用開發封閉原則,可以給我們更多的收穫,首先將銀行系統中最可能擴展的部分隔離出來,形成統一的接口處理,在銀行系統中最可能擴展的因素就是業務功能的增加或變更。對於業務流程應該將其作為可擴展的部分來實現,當有新的功能增加時,不需重新梳理已經形成的業務接口,然後再整個系統要進行大的處理動作,那么怎么才能更好的實現耦合度和靈活性兼有的雙重機制呢?
答案就是抽象。將業務功能抽象為接口,當業務員依賴於固定的抽象時,對於修改就是封閉的;而通過繼承和多態機制,從抽象體派生出新的擴展實現,就是對擴展的開放。
依據開放封閉原則,進行重構,新的設計思路如圖2所示。
圖2 面向抽象的設計
按照上述設計實現,用細節表示為:
interface IBankProcess
{
void Process();
}
然後在隔離的接口上,對功能進行擴展,例如改造單一職責的示例將有如下的實現:
// 按銀行按業務進行分類
class DepositProcess : IBankProcess
{
//IBankProcess Members
#region IBankProcess Members
public void Process()
{
// 辦理存款業務
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
class TransferProcess : IBankProcess
{
//IBankProcess Members
#region IBankProcess Members
public void Process()
{
// 辦理轉賬業務
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
class DrawMoneyProcess : IBankProcess
{
//IBankProcess Members
#region IBankProcess Members
public void Process()
{
// 辦理取款業務
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
這種思路的轉換,會讓複雜的問題變得簡單明了,使系統各負其責,人人實惠。有了上述的重構,銀行工作人員徹底變成一個EasyBankStaff(“輕鬆”的組織者):
class EasyBankStaff
{
private IBankProcess bankProc = null;
public void HandleProcess(Client client)
{
bankProc = client.CreateProcess();
bankProc.Process();
}
}
銀行業務可以像這樣被自動地實現了:
class BankProcess
{
public static void Main()
{
EasyBankStaff bankStaff = new EasyBankStaff();
bankStaff.HandleProcess(new Client("轉賬用戶"));
}
}
你看,現在一切都變得輕鬆自在,匆忙中辦理業務的人們不會在長長的隊伍面前一籌莫展,而業務員也從繁瑣複雜的勞動中解脫出來。當有新的業務增加時,銀行經理不必為重新組織業務流程而擔憂,你只需為新增的業務實現IBankProcess接口,系統的其他部分將絲毫不受影響,辦理新業務的客戶會很容易找到受理新增業務的視窗,如圖5所示。
圖5符合OCP的設計
對應的實現為:
class FundProcess : IBankProcess
{
//IBankProcess Members
#region IBankProcess Members
public void Process()
{
// 辦理基金業務
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
可見,新的設計遵守了開放封閉原則,在需求增加時只需要向系統中加入新的功能實現類,而原有的一切保持封閉不變的狀態,這就是基於抽象機制而實現的開放封閉式設計。
然而,細心觀察上述實現你會發現一個非常致命的問題:人們是如何找到其想要處理的業務視窗,難道銀行還得需要一部分人力來進行疏導?然而確實如此,至少當前的設計必須如此,所以上述實現並非真正的業務處理面貌,實際上當前“輕鬆”的銀行業務員,還並非真正的“輕鬆”,我們忽略了這個業務系統中最重要的一部分,就是用戶。當前,用戶的定義被實現為:
class Client
{
private string ClientType;
public Client(string clientType)
{
ClientType = clientType;
}
public IBankProcess CreateProcess()
{
switch (ClientType)
{
case "存款用戶":
return new DepositProcess();
break;
case "轉賬用戶":
return new TransferProcess();
break;
case "取款用戶":
return new DrawMoneyProcess();
break;
}
return null;
}
}
如果出現新增加的業務,你還必須在長長的分支語句中加入新的處理選項,switch的壞味道依然讓每個人看起來都倒胃口,銀行業務還是以犧牲客戶的選擇為代價,難道不能提供一個自發組織客戶尋找業務視窗的機制嗎?
其中的設計原則就是用於解決上述問題的。
規則建議
l 開放封閉原則,是最為重要的設計原則,Liskov替換原則和合成/聚合復用原則為開放封閉原則的實現提供保證。
l 可以通過Template Method模式和Strategy模式進行重構,實現對修改封閉、對擴展開放的設計思路。
l 封裝變化,是實現開放封閉原則的重要手段,對於經常發生變化的狀態一般將其封裝為一個抽象,例如銀行業務中的IBankProcess接口。
l 拒絕濫用抽象,只將經常變化的部分進行抽象,這種經驗可以從設計模式的學習與套用中獲得。