變化的平衡點----BrainStorm

[color=red][size=medium]PS:如下內容純屬個人想法,有問題,請輕拍![/size][/color]

早些時候在罈子裏看到了這樣一個帖子(具體帖子鏈接找不到了)。內容是這樣的。有一個需求:要用*打印出三行的一個三角形。具體怎麼實現。相信看到這個題目,有語言基礎的人,第一反應就是兩個循環,搞定。作者給出的答案如下:

System.out.println(" *");
System.out.println(" ***");
System.out.println("*****");

第一眼看了可能很好笑,但看了作者的解釋的確如此。用戶永遠是bt的,需求永遠是變化的。

而最近在跟chjavach的設計模式,個人感覺橋接模式寫得最好,我看了以後思路很清晰,其他模式看完沒有那麼清晰的感覺。而[url=http://chjavach.iteye.com/blog/834679]簡單工廠[/url]裏的這一幅圖則是醍醐灌頂啊。讓我終於知道了Factory的存在理由了。這可能和個人的開發經歷有關,一般都是一個人包攬前後臺,而不是分工合作的原因吧。

[img]http://dl.iteye.com/upload/attachment/368898/58ceb91c-2733-3b01-82fa-46192b04a234.jpg[/img]

設計模式說到底是用來封裝變化的,就對上面的題目來看,如果需求沒有變化,那麼上面的實現是完全可以的。而當需求變化的時候再去重構吧---是重構,不是簡單的修改。好像是Bob的《敏捷軟件開發:原則、模式與實踐》這本書裏看到的吧!不要讓子彈擊中你兩次。也就是說第一次以最快實現方式實現需求,當需求更改時,進行重構。

前段時間看了不少書,數據結構,算法,python,c,c++,gog還有雜七雜八的書籍,當然都沒怎麼深究了。然後某天晚上睡覺時就想了不少問題!需求是不斷變化的。那什麼時候使用設計模式?怎麼就知道要使用什麼樣的設計模式了?如果像敏捷開發所說的,重構出模式,什麼時候重構?有多大風險?是否不同的語言對變化的解決方案不同呢?如果相同,各位大師發明這麼多語言幹嘛?如果不同,怎麼個不同法呢?

相信看過HeadFirst設計模式的人對第一章應該印象都比較深刻吧!這就是一個由需求變更而導致重構進而演化出了策略模式的例子。

這裏引用一下他的例子,不過從更原始的模型開始。一開始只有一隻鴨子。它能quake,swing,display。如下圖所示:

[img]http://dl.iteye.com/upload/attachment/368958/cf383ab8-9ab4-3213-a431-1b5ff3a72f2c.jpg[/img]

代碼如下:

public class Duck {
public String quack() {
return "GaGa";
}

public String swing() {
return "Swing";
}

public String display() {
return "Duck Display";
}
}


測試一下:

public class DuckTest {

private Duck duck ;
@Before
public void setUp() throws Exception {
duck = new Duck();
}

@After
public void tearDown() throws Exception {

}

@Test
public void testQuack() throws Exception {
assertEquals("GaGa",duck.quack());
}

@Test
public void testSwing() throws Exception {
assertEquals("Swing",duck.swing());
}

@Test
public void testDisplay() throws Exception {
assertEquals("Duck Display",duck.display());
}
}


這裏的Duck就是server端的代碼,測試類則相當於client端的代碼。

這段代碼肯定是沒有問題的,太簡單了。如果所有的項目都這麼簡單,大家都笑了。

接着出現了第二隻鴨子,它是隻紅頭鴨子,它也能quack,swing和display.不同的地方是紅頭鴨子的display顯示的是"RedDuck Display",那麼這裏能想到的就是繼承了,讓RedDuck繼承Duck,繼而覆寫Duck的display方法就可以了。

[img]http://dl.iteye.com/upload/attachment/368979/ac73d0bd-4bb9-3229-8885-7f7402a896ca.jpg[/img]

然後又是,GreenDuck啦之類的,類圖變成這樣。

[img]http://dl.iteye.com/upload/attachment/368986/095a6d3a-0791-3148-955c-ebfbf3de5775.jpg[/img]

以後如果要添加鴨子,只要添加相應的鴨子類,繼承Duck,覆寫Duck的display方法就可以了。
可以看出,這樣的設計對這樣的需求是符合要求的。符合開閉原則---對修改關閉,對擴展開放。我們來看一下,這段代碼怎麼對修改關閉,對擴展開放了。在server端,當需要添加鴨子的時候,只需要繼承Duck即可。不需要修改現有的任何代碼。有人就問了,那client端呢?不是要改代碼嗎?(以前我也有這樣的疑問)。同樣是看了上面那幅Factory的圖我豁然開朗。當區分開了client和server端後,知道了開閉原則是針對server端的。對於client端,你要調用server端新添加的鴨子當然要改源代碼了,不然怎麼調用?當然server端可以封裝個Factory提供出來。源代碼多多少少都是需要修改的。

對於這樣的設計呢!OO語言都是沒什麼太大的區別的,C++,Ruby實現都很類似了。

第一個平衡點:
這裏可能會有一些聲音,說對於client來說,調用Duck的時候需要new纔可以,所以要個Factory。這裏可能就是斟酌的地方了。上面已經說了,模式是用來封裝變化的。如果這幾個Duck類都不會變化,那麼client使用new,也沒什麼問題。而問題是你沒辦法保證這些Duck不會發生變化。從另一個角度來看,你也不能保證這些Duck一定就會發生變化,如果所有的地方不分青紅皁白全用Factory,那麼就會Factory類爆炸了吧。而如果都不用Factory類,如果Duck類發生了變化,那麼當類的數量變化得很多時,修改就是個噩夢了吧。

第二個平衡點:
接着呢!出現了WoodDuck了,他是不會quack的。繼續上面的方式--繼承。覆寫quack方法,空實現即可。再來一個PaperDuck呢?繼續繼承?空實現?這裏就出現了重複了。而這裏應該又是一個平衡點了。HeadFirst是假設此類不會quack的Duck越來越多的話,重複代碼就越來越多了,繼而進行了重構。我覺得這裏有幾個問題
1.此處完全是在假設的前提下進行重構的。如果類沒繼續增加呢?
2.如果要重構,到底多少個類似的Duck出現時需要重構呢?
3.假設根據DRY原則,出現第二個的時候就進行重構是否有些勞師動衆?如果Duck已經繼承了100甚至更多的類了,而且類運行得很好,此時出現了WoodDuck和PaperDuck進行重構的話,風險有多大呢?
4.有測試呢!你能保證測試覆蓋全面嗎?!

我們繼續往下看!假設現在已經有100個類似RedDuck,GreenDuck的類繼承了Duck了,測試類覆蓋完全。此時出現了WoodDuck和PaperDuck類,這個時候再看HeadFirst裏面的關於抽象出接口的實現方式,應該一眼就看出他的弊端了。抽象出一個Quackable接口,能Quack的就實現這個接口,不能Quack的就不實現這個接口。好吧,現在有100只鴨子能Quack,2只不能Quack,你如此重構看看。多了100個重複(誰讓接口不能實現方法呢!)。。。知道什麼叫吃力不討好了嗎?

這時候C++笑了,讓你不能多繼承。看我,直接抽出Quack父類,誰能Quack就繼承Quack。不能Quack的就不繼承。

Ruby也笑了,直接將quack放到Quack模塊裏面去,誰能quack就mixin Quack模塊唄。

再回到Java,這裏就開始關注變化點!很明顯,這裏的不穩定因素是quack,有的鴨子能quack有的則不能,我們則把quack抽出來,獨立爲一個類。當需要quack的時候就設置這個quack即可。這就是策略模式。

第三個平衡點:
策略模式應該也分爲兩種,我自己把它稱作server端的策略和client端的策略。server端的策略就是將quack實例化在了相應的duck內部,client端直接實例化即可。而client端的策略就是在client端自己設置相應的quack到duck中去。很明顯,client端的策略更靈活。但是這裏不適用。看看這裏已經有100多個類了,如果每個類都修改爲client端的策略模式,那麼修改量太大,收效也不明顯。當然也可以混合使用,默認提供了server端的策略,也提供client端的策略,供client端靈活調用。

對於server端的策略實現,我們和c++,ruby的實現比較一下。假如,GreenDuck的quack需要修改了,策略模式是將duck內的quack實例替換掉。c++是將其quack父類修改掉(這裏的父類肯定也是有個繼承關係的,都有共同的父類,否則client端怎麼調用呢?)。而ruby呢,直接添加一個新的quack模塊,替換原來那個模塊就可以了。

所以,c++的多繼承,ruby的mixin也同樣的解決了問題。但是,c++和ruby從語言特性級別解決了java需要使用設計模式才能解決的問題。當然了,c++,ruby也是能實現策略模式的。


總結:
1. 模式也不是亂用的。對於第一個平衡點,可能會導致Factory爆炸。對於第二個平衡點,可能會加大工作量和風險。怎麼平衡,看項目,看需求,看個人經驗。

2. 模式需要結合實際情況。像上面的例子,c++,ruby從語言級別就可以解決問題了,不需要再升級到模式的級別。

前段時間看了一本書《怪誕行爲學》,裏面提到了一些關於暗示的例子,不知道用在這裏合不合適。例子是這樣的,一家出版社網站打印瞭如下的書籍價格:《書籍A》pdf版本:$59,《書籍A》實體書:$120,《書籍A》pdf+實體書:$120。多少人會買$120的實體書呢?選項2暗示了你,選項3比選項1要划算!而在這裏,當我們學完OO,書上說繼承怎麼樣怎麼樣好的時候,我們的心裏其實已經受到了影響,有問題就繼承。再學習了模式後,又一次被暗示了,有問題找模式。繼承和模式不過是一種結構而已,有適用的地方,具體哪裏適用,需要我們來掌握。怎麼掌握?經驗。。。。也許這就是畢業生和資深開發人員的本質區別吧。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章