設計模式初學者系列-策略模式
??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? -------為什么總是繼承
模板方法的延續
這篇稿子是基于我的前一篇模板方法設計模式之上演繹的,如果沒有閱讀請點擊這里查看,以了解這篇稿子的上下文。
在模板方法設計模式里我舉了一個例子:教育部規定了新生報到流程的算法骨架,然后這個算法骨架中的一些關鍵步驟由各高校自由的去發揮。我在這個例子中將高校設為一個抽象類,各高校要實現的算法步驟都是抽象方法。我還給出了兩個高校的實現代碼:清華大學和北京大學。在這個例子中本沒有什么問題,但是軟件總是會變的。
當有更多的高校要實現的時候,我們就會發現,很多高校的有些報到步驟實現是一樣的,這就存在子類中有大量的重復代碼,重復總是會出問題的。當然我們可以使用Martin Fowler的Pull Up Method(Refactoring P320)重構方法,將這些共同的部分推移到高校這個父類實現,并將這個抽象類改為virtual。
public abstract class 高校
{
public void 報到()
{
教務處報到();
繳費();
本院系報到();
教材科發教材();
}
protected abstract void 教務處報到();
//方法由抽象的更改為虛方法
protected virtual void 繳費()
{
//將這個方法在父類去實現,因為好多高校的實現都是這樣的,避免重復
}
protected abstract 專業等信息 本院系報到();
protected abstract 教材 教材科發教材();
}
但是,現在出現了這樣的情況:A,B,C等幾個大學的實現在某些步驟上有些相同,D,F在某些步驟的實現有些相同,也許你會說:這不好辦,繼續使用繼承唄,將共同的東西往上推,并且在“高校類”和各高校實現的類中間插入一些類,這些類將提供共同的實現。好像是個很好的辦法。來瞧一瞧:
重復的代碼確實減少了很多,但是還有一些重復(心里在默默的罵道:TMD,為什么C#不支持多繼承,不然我就可以消除重復了),也許你還在自我陶醉的欣賞著自己多么完美的類繼承層次,在那里感慨OO的強大。但是隨著具體的高校越來越多,而且有的高校的報到步驟居然要發生改變,你小心的在中間那一層添加新的類,并將一些高校的實現轉移,每一次你都非常小心(這個系統正在高速的運轉,每改錯一步,就有多少莘莘學子入不了學)。你心里終于對OO不滿起來:為什么,為什么大家都說OO是救世主,但是卻救不了我。答案是因為你將OO的設計原則遺忘在課本里了。開閉原則、優先使用組合,你還記得嗎?
在我們很多OO程序員的腦子里總是存在這樣一個觀念:沒有繼承的程序不是OO的程序,看到重復總是想到繼承。當初我也是這樣想的,有的時候看到自己畫的龐大的繼承類圖,心里在樂呵呵的笑。可繼承總是不給面子,一個小小的變化就將這個看似穩定的體系弄的支離破碎。
還是回到我們的例子,在這個例子中變化的是各高校的報到步驟,本著發現變化、封裝變化、隔離變化的原則我們將報到的步驟分離出來,獨立成類。
這樣我們就可以復用這些步驟了,有新的步驟實現只要添加更多的子類,并不需要修改原來的代碼。(作業:在繼續閱讀之前根據上面的類圖自己寫出實現的代碼來)
這就是所謂的策略設計模式:策略模式定義了算法族,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化獨立于算法的客戶(DP)。
策略模式有三種參與者:
一、 Context 這個類保存了對策略的引用,并且調用實際的策略實現,有可能還提供一個接口,讓策略可以訪問它內部的數據,在這里就是我們的“高校”類。
二、 Strategy 策略接口,給算法族定義一個通用的接口,讓客戶以一種一致的方法去訪問。(I教務處報到,I繳費)
三、 ConcreteStrategy 這就是具體的策略實現了,實現策略接口(各報到步驟的實現)。
如下圖:
在我們的例子中報到的步驟就是算法族 比如“繳費”這個步驟,有多種繳費方式,我們將其封裝起來,客戶調用的時候并不需要了解你是怎么實現這個“繳費”的,這個過程對于客戶來說是透明的。這些不同的“繳費”步驟之間是可以無縫的替換,而客戶對此一點都不知覺。
好了,既然解決方案提出來了,我們就來實現它吧
首先我們定義所有的報到步驟的接口:
public interface I教務處報到
{
void 教務處報到();
}
public interface I繳費
{
void 繳費();
}
public interface I本院系報到
{
void 本院系報到();
}
public interface I教材科發教材
{
void 教材科發教材();
}
下面我實現兩個教務處報到的步驟,其他的就當作課后作業了,呵呵。
public class 教務處報到A : I教務處報到
{
public void 教務處報到()
{
Console.Write("教務處報到,A類實現");
}
}
public class 教務處報到B : I教務處報到
{
public void 教務處報到()
{
MessageBox.Show("教務處報到,B類實現");
}
}
再看看我們的高校類的實現吧:
public class 高校
{
public 高校(I教務處報到 p教務處報到,I繳費 p繳費,I本院系報到 p本院系報到,I教材科發教材 p教材科發教材)
{
this._教務處報到 = p教務處報到;
this._繳費 = p繳費;
this._本院系報到 = p本院系報到;
this._教材科發教材 = p教材科發教材;
}
//為什么有了賦值的構造函數還要暴露這么多只寫屬性出來呢?
//這樣就可以在運行時改變高校的報到步驟了,
//假如報到系統出現故障我們可以馬上采取另外一種方案
//而不需要停止系統的運行
I教務處報到 _教務處報到;
public I教務處報到 教務處報到
{
set
{
_教務處報到 = value;
}
}
I繳費 _繳費;
public I繳費 繳費
{
set
{
_繳費 = value;
}
}
I本院系報到 _本院系報到;
public I本院系報到 本院系報到
{
set
{
_本院系報到 = value;
}
}
I教材科發教材 _教材科發教材;
public I教材科發教材 教材科發教材
{
set
{
_教材科發教材 = value;
}
}
//用上了策略模式,模板方法更加靈活了
//但現在還是不是模板方法了?
public void 報到()
{
教務處報到.教務處報到();
繳費.繳費();
本院系報到.本院系報到();
教材科發教材.教材科發教材();
}
}
Ok,我就把代碼寫這么多了,要這個代碼運行起來還需要一些補充,這個高校類如何進行實例化才能更靈活也值得考慮。
看到沒,利用組合我們也可以達到代碼復用的目的,而且沒有繼承的弊端。
上面好像都是在說策略模式的好話,那策略模式有沒有副作用呢?當然有
一、 雖說客戶代碼無須關心各個策略是如何實現的,但是它們還是要知道有多少種策略實現,該實現是干什么的,也就是客戶代碼需要知道策略的一些細節,這樣才可以根據需要使用哪個策略,但是我們可以使用創建型模式來解決這個問題。
二、 有的時候策略需要從Context那里獲取一些數據,這樣造成雙向的關聯,而且有可能幾個策略需要的數據都不一樣,但是為了一致性不得不向它們傳遞相同的數據。
三、 也許大家會發現,使用策略模式后出現很多小類,實際上這也是所有設計模式的“通病”。
現實中的策略模式
大家對于PetShop這個應用肯定很熟悉,在PetShop 4.0里面就使用了策略設計模式:
在Petshop4的BLL項目中有一個OrderAsynchronous類和一個OrderSynchronous類,它們都繼承自IorderStrategy。OrderSynchronous以一種同步的方式處理訂單,而OrderAsynchronous先將訂單放在隊列里,然后再對隊列里的訂單進行處理,以一種異步方式。而在BLL中的Order類里通過反射從配置文件讀取策略配置的信息以決定到底是使用哪種訂單處理方式。這里就不貼代碼了,有興趣的可以去下載PetShop看看,主要關注這幾個:PetShop.BLL.Order(如何使用策略以及如何根據配置文件實例化具體的策略)、PetShop.IBLLStrategy. IorderStrategy(策略的接口)、PetShop.BLL. OrderSynchronous、PetShop.BLL. OrderAsynchronous。
總結
在本篇我們從模板方法談起,聊了一些模板方法隨著項目的發展可能造成的問題,但這并不是模板方法的弊端,模板方法關注的是算法骨架的復用,如果你發覺新的問題出現,這可能就是模板方法不再適用的信號。通過我們對項目的擴展,發現繼承在某些時候并不是都能達到代碼復用的目的,這個時候我們應該考慮組合了,而且繼承是一種靜態的編譯期的行為(針對像C#這種強類型靜態語言而言),代碼一經寫定我們就沒有選擇的余地了。
前幾天和別人在群里閑聊,談到怎樣學習設計模式,有人說設計模式靠悟,有人說設計模式靠經驗的積累。悟也好,經驗積累也好,我的感覺是不要把設計模式當作圣經,當一個人把一個事物當作圣經的時候總是很珍惜她,而且不會去褻瀆她,這是學習模式的障礙。對于初學者來說應該有“熟讀唐詩三百首,不會吟詩也會吟”的決心。