開放封閉原則

2.3.1 引言

無論如何,開放封閉原則(OCPOpen Closed Principle)都是所有面向對象原則的核心。軟件設計本身所追求的目標就是封裝變化、降低耦合,而開放封閉原則正是對這一目標的最直接體現。其他的設計原則,很多時候是爲實現這一目標服務的,例如以Liskov替換原則實現最佳的、正確的繼承層次,就能保證不會違反開放封閉原則。

2.3.2 引經據典

關於開發封閉原則,其核心的思想是:

軟件實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。

因此,開放封閉原則主要體現在兩個方面:

對擴展開放,意味着有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。

對修改封閉,意味着類一旦設計完成,就可以獨立完成其工作,而不要對類進行任何修改。

“需求總是變化”、“世界上沒有一個軟件是不變的”,這些言論是對軟件需求最經典的表白。從中透射出一個關鍵的意思就是,對於軟件設計者來說,必須在不需要對原有的系統進行修改的情況下,實現靈活的系統擴展。而如何能做到這一點呢?

只有依賴於抽象。實現開放封閉的核心思想就是對抽象編程,而不對具體編程,因爲抽象相對穩定。讓類依賴於固定的抽象,所以對修改就是封閉的;而通過面向對象的繼承和對多態機制,可以實現對抽象體的繼承,通過覆寫其方法來改變固有行爲,實現新的擴展方法,所以對於擴展就是開放的。這是實施開放封閉原則的基本思路,同時這種機制是建立在兩個基本的設計原則的基礎上,這就是Liskov替換原則和合成/聚合複用原則。關於這兩個原則,我們在本書的其他部分都有相應的論述,在應用反思部分將有深入的討論。

對於違反這一原則的類,必須進行重構來改善,常用於實現的設計模式主要有Template Method模式和Strategy模式。而封裝變化,是實現這一原則的重要手段,將經常發生變化的狀態封裝爲一個類。

2.3.3 應用反思

站在銀行窗口焦急等待的用戶,在長長的隊伍面前顯得無奈。所以,將這種無奈遷怒到銀行的頭上是理所當然的,因爲銀行業務的管理顯然有不當之處。銀行的業務人員面對蜂擁而至的客戶需求,在排隊等待的人們並非只有一種需求,有人存款、有人轉賬,也有人申購基金,繁忙的業務員來回在不同的需求中穿梭,手忙腳亂的尋找各種處理單據,電腦系統的功能模塊也在不同的需求要求下來回切換,這就是一個發生在銀行窗口內外的無奈場景。而我每次面對統一排隊的叫號系統時,都爲前面長長的等待人羣而叫苦,從梳理銀行業務員的職責來看,在管理上他們負責的業務過於繁多,將其對應爲軟件設計來實現,你可以將這種拙劣的設計表示如圖2-3所示。


按照上述設計的思路,銀行業務員要處理的工作,是以這種方式被實現的:

None.gifclass BusyBankStaff
ExpandedBlockStart.gif
{
InBlock.gif
private BankProcess bankProc =new BankProcess();
InBlock.gif
// 定義銀行員工的業務操作
InBlock.gif
publicvoid HandleProcess(Client client)
ExpandedSubBlockStart.gif
{
InBlock.gif
switch (client.ClientType)
ExpandedSubBlockStart.gif
{
InBlock.gif
case"存款用戶":
InBlock.gif                    bankProc.Deposit();
InBlock.gif
break;
InBlock.gif
case"轉賬用戶":
InBlock.gif                    bankProc.Transfer();
InBlock.gif
break;
InBlock.gif
case"取款戶":
InBlock.gif                    bankProc.DrawMoney();
InBlock.gif
break;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

ExpandedBlockEnd.gif}

None.gif

這種設計和實際中的銀行業務及其相似,每個BusyBankStaff(“繁忙的”業務員)接受不同的客戶要求,一陣手忙腳亂的選擇處理不同的操作流程,就像示例代碼中的實現的Switch規則,這種被動式的選擇造成了大量的時間浪費,而且容易在不同的流程中發生錯誤。同時,更爲嚴重的是,再有新的業務增加時,你必須修改BankProcess中的業務方法,同時修改Switch增加新的業務,這種方式顯然破壞了原有的格局,以設計原則的術語來說就是:對修改是開放的。

以這種設計來應對不斷變化的銀行業務,工作人員只能變成BusyBankStaff了。分析這種僵化的代碼,至少有以下幾點值得關注:銀行業務封裝在一個類中,違反單一職責原則;有新的業務需求發生時,必須通過修改現有代碼來實現,違反了開放封閉原則。

解決上述麻煩的唯一辦法是應用開放封閉原則:對擴展開放,對修改封閉。我們回到銀行業務上看:爲什麼這些業務不能做以適應的調整呢?每個業務員不必周旋在各種業務選項中,將存款、取款、轉賬、外匯等不同的業務分窗口進行,每個業務員快樂地專注於一件或幾件相關業務,就會輕鬆許多。綜合應用單一職責原則來梳理銀行業務處理流程,將職責進行有效的分離;而這樣仍然沒有解決業務自動處理的問題,你還是可以聞到僵化的壞味道在系統中瀰漫。

應用開發封閉原則,可以給我們更多的收穫,首先將銀行系統中最可能擴展的部分隔離出來,形成統一的接口處理,在銀行系統中最可能擴展的因素就是業務功能的增加或變更。對於業務流程應該將其作爲可擴展的部分來實現,當有新的功能增加時,不需重新梳理已經形成的業務接口,然後再整個系統要進行大的處理動作,那麼怎麼才能更好的實現耦合度和靈活性兼有的雙重機制呢?

答案就是抽象。將業務功能抽象爲接口,當業務員依賴於固定的抽象時,對於修改就是封閉的;而通過繼承和多態機制,從抽象體派生出新的擴展實現,就是對擴展的開放。

依據開放封閉原則,進行重構,新的設計思路如圖2-4所示。

2-4 面向抽象的設計

按照上述設計實現,用細節表示爲:

None.gifinterface IBankProcess
ExpandedBlockStart.gif
{
InBlock.gif
void Process();
ExpandedBlockEnd.gif}

None.gif

然後在隔離的接口上,對功能進行擴展,例如改造單一職責的示例將有如下的實現:

// 按銀行按業務進行分類

None.gifclass DepositProcess : IBankProcess
ExpandedBlockStart.gif
{
ContractedSubBlock.gif
IBankProcess Members
ExpandedBlockEnd.gif }

None.gif
None.gif
class TransferProcess : IBankProcess
ExpandedBlockStart.gif
{
ContractedSubBlock.gif
IBankProcess Members
ExpandedBlockEnd.gif }

None.gif
None.gif
class DrawMoneyProcess : IBankProcess
ExpandedBlockStart.gif
{
ContractedSubBlock.gif
IBankProcess Members
ExpandedBlockEnd.gif }

None.gif

這種思路的轉換,會讓複雜的問題變得簡單明瞭,使系統各負其責,人人實惠。有了上述的重構,銀行工作人員徹底變成一個EasyBankStaff(“輕鬆”的組織者):
None.gifclass EasyBankStaff
ExpandedBlockStart.gif
{
InBlock.gif
private IBankProcess bankProc =null;
InBlock.gif
InBlock.gif
publicvoid HandleProcess(Client client)
ExpandedSubBlockStart.gif
{
InBlock.gif            bankProc
= client.CreateProcess();
InBlock.gif            bankProc.Process();
ExpandedSubBlockEnd.gif        }

ExpandedBlockEnd.gif }

None.gif

銀行業務可以像這樣被自動地實現了:


None.gifclass BankProcess
ExpandedBlockStart.gif
{
InBlock.gif
publicstaticvoid Main()
ExpandedSubBlockStart.gif
{
InBlock.gif            EasyBankStaff bankStaff
=new EasyBankStaff();
InBlock.gif            bankStaff.HandleProcess(
new Client("轉賬用戶"));
ExpandedSubBlockEnd.gif        }

ExpandedBlockEnd.gif }

None.gif

你看,現在一切都變得輕鬆自在,匆忙中辦理業務的人們不會在長長的隊伍面前一籌莫展,而業務員也從繁瑣複雜的勞動中解脫出來。當有新的業務增加時,銀行經理不必爲重新組織業務流程而擔憂,你只需爲新增的業務實現IBankProcess接口,系統的其他部分將絲毫不受影響,辦理新業務的客戶會很容易找到受理新增業務的窗口,如圖2-5所示。


2-5 符合OCP的設計

對應的實現爲:


None.gifclass FundProcess : IBankProcess
ExpandedBlockStart.gif
{
ContractedSubBlock.gif
IBankProcess Members
ExpandedBlockEnd.gif}

None.gif

可見,新的設計遵守了開放封閉原則,在需求增加時只需要向系統中加入新的功能實現類,而原有的一切保持封閉不變的狀態,這就是基於抽象機制而實現的開放封閉式設計。

然而,細心觀察上述實現你會發現一個非常致命的問題:人們是如何找到其想要處理的業務窗口,難道銀行還得需要一部分人力來進行疏導?然而確實如此,至少當前的設計必須如此,所以上述實現並非真正的業務處理面貌,實際上當前“輕鬆”的銀行業務員,還並非真正的“輕鬆”,我們忽略了這個業務系統中最重要的一部分,就是用戶。當前,用戶的定義被實現爲:


None.gifclass Client
ExpandedBlockStart.gif
{
InBlock.gif
privatestring ClientType;
InBlock.gif
InBlock.gif
public Client(string clientType)
ExpandedSubBlockStart.gif
{
InBlock.gif            ClientType
= clientType;
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif
public IBankProcess CreateProcess()
ExpandedSubBlockStart.gif
{
InBlock.gif
switch (ClientType)
ExpandedSubBlockStart.gif
{
InBlock.gif
case"存款用戶":
InBlock.gif
returnnew DepositProcess();
InBlock.gif
break;
InBlock.gif
case"轉賬用戶":
InBlock.gif
returnnew TransferProcess();
InBlock.gif
break;
InBlock.gif
case"取款用戶":
InBlock.gif
returnnew DrawMoneyProcess();
InBlock.gif
break;
ExpandedSubBlockEnd.gif            }

InBlock.gif
returnnull;
ExpandedSubBlockEnd.gif        }

ExpandedBlockEnd.gif    }

None.gif

如果出現新增加的業務,你還必須在長長的分支語句中加入新的處理選項,switch的壞味道依然讓每個人看起來都倒胃口,銀行業務還是以犧牲客戶的選擇爲代價,難道不能提供一個自發組織客戶尋找業務窗口的機制嗎?

我們把答案放在下一節2.4節“依賴倒置原則”,其中的設計原則就是用於解決上述問題的。我們對於銀行業務的討論,還會繼續進行。

2.3.4 規則建議

l開放封閉原則,是最爲重要的設計原則,Liskov替換原則和合成/聚合複用原則爲開放封閉原則的實現提供保證。

l可以通過Template Method模式和Strategy模式進行重構,實現對修改封閉、對擴展開放的設計思路。

l封裝變化,是實現開放封閉原則的重要手段,對於經常發生變化的狀態一般將其封裝爲一個抽象,例如銀行業務中的IBankProcess接口。

l拒絕濫用抽象,只將經常變化的部分進行抽象,這種經驗可以從設計模式的學習與應用中獲得。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章