2012年3月18日 星期日

[Programming] 設計模式 (1) - 策略模式 (Strategy Pattern)

さ~我們就來看看抽象(abstract)和介面(interface)的設計應用,看看它如何讓程式變得彈性又功能強大,而把物件導向程式帶領到另一個次元去~

在開始之前,你可能會需要先看看之前幾篇[1],了解一下物件導向程式設計的起因和操作。
所以在這裡,是都假設對抽象、介面這些都明白了,只是還不知道怎麼去應用在實務上而已。
另外,class都沒建的程式設計方式就不在這裡討論了。

----廢話的分隔線----

首先呢,我們來看看我們的需求。
  1. 建構一個"財產"類別。
  2. 已知的財產很多種,如土地、土地改良物、機械設備、交通設備、雜項、物品等。
  3. 會需要對財產進行異動,如新增、減損、重估等,異動流程有些一樣,有些不同。
  4. 日後要能夠對財產種類和異動種類進行擴充。
看起來算是很實際的例子吧?如果有觀眾在做ERP,這種案例應該隨時會遇到。
這個例子,也是我在出社會後第一個進行開發的專案。雖然還是有些許的不一樣,但大體上差不多啦。

這個,應該就能夠滿足他的需求....
如果你是哪個經理還是老闆,你覺得你的員工壓榨起來讓你很舒服,或是做出來的軟體日後不需要維護,你就可以這樣做。
我光是畫這張圖就累了!除非拿槍指在我的頭上,否則我無法接受我自己寫出來的程式長這個鳥樣子!
但是,相信我,一定還是有些人會這麼做。當我和沒有正確程式設計觀念的人共事時,就發現沒有什麼事是不可能的。

這個時候,我們應該平心靜氣地,把這些類別中相同的部分取出來,讓各個類別繼承。
然後就變成下面那樣子....
我們先把各個財產都有的屬性全都拿出來,放到一個Property(財產)抽象類別去,然後讓每一種財產都去繼承。這樣子看起來,好像還不錯?至少變數都拿到抽象類別去了。

然而,尷尬的點在於下面的那些財產,他們還有異動的方法還沒抽出來。如果是上面那樣的話,等於我在每個類別裡都要再另外寫異動方法;那如果是下面那樣....
好像就可以不用重寫太多東西了?會不會有哪裡怪怪的呢?
需求的點在於:異動流程有些一樣,有些不同。我們應該把哪些一樣的流程丟到Property去?哪些放在類別本身?
需求又說:日後要能夠對財產種類和異動種類進行擴充。這就表示,根本沒辦法知道哪些流程重複性比較高,哪些流程比較少用。

我們學過一個東西叫做介面 (interface),能夠強迫每一個實作它的類別實作一些方法。所以,我們還會看到下面那種東西…
如果你學了一套新方法,不確定怎麼用的話就不要亂用。幕容復就領悟過:再怎麼華麗的招式,也只是招式上佔上風,如果使用這些招式會因不熟練或其它因素而減弱傷害力,還不如不要用。這也是為什麼,日本的劍道和空手道的上段高手,每次練習的時候還是有再練基本的揮劍和正拳中段攻擊。

扯遠了。反正,如果一種方法不會讓你的工作量變少,就不要亂導入。看看上面那張圖,想像一下:這和第二張,剛把變數抽出來放到Property裡有什麼不一樣。
沒什麼不一樣!這樣還是得在每個類別裡實作異動方法啊!!
還有,不要覺得複製貼上程式碼是很容易的事。這種事情做多了,就像用火燒藤甲兵一樣,會損陽壽的。等到哪天業主一道指令叫你改掉什麼東西,你會發現你的時間都浪費在以前不知道複製幾次的程式碼上面。

----各種可憐情境的分隔線----

GG (good-bye garbage),跟那些爛code說再見吧!這個時候,就可以看到優秀的程式員如何用知識解決問題(工作量大的問題??)。
我們來整理一下問題。前面遺留下來的困難點在於那一堆神鬼莫測的異動流程,因為有些財產用的異動流程有一樣,有些不一樣,所以很難去特別把哪一種異動流程抓出來繼承。
那麼,就全部抽出來好了。

設計守則第二點就寫著[2][3]:
Program to an interface, not an implementation.
寫程式是針對介面而寫,而不是針對實踐而寫。
原因則是....
以前的作法是:行為是繼承自超類別的實踐方式而來或是繼承某個介面並由次類別自行實踐而來。這兩種作法都是依賴「實踐」,我們被實踐綁得死死的,沒辦法更改行為(除非寫更多的程式碼)。
所以,我們接下來的作法將迥異於以往。
我們另外做一個Operator的介面,然後把各種異動流程方法獨立出來,並且實踐該介面。這麼做,無論是Property或是Land...等財產類別,都不會被這些演算法套牢了。這就是「程式是對介面而寫」,或是「對超型態而寫」。
這樣子設計,財產那裡需要什麼異動方法,就加在這裡就好了,和財產本身就一點關係也沒有。各類財產需要哪些異動方法,再自己呼叫就行了。

於是,經過整合以後,我們就可以得到這個圖....
看見沒?那些有的沒的異動方法全和財產分開了!而Property那裡和Operator有關的,就是定義一個Operator類型的屬性,而財產本身如果要做什麼異動的時候,把各個演算法定義清楚,就可以操作了。

就像這樣....
//土地財產
public class Land extends Property {

    ///Constructor
    public Land(){
        ID = "";
        PropNo = "";
        PropNoSeq = "";
        cost =0;
        val = 0;
    }
    
    ///不動產新增流程
    public boolean Add(){
        operator = new EstateAdd();
        return operator.operate();
    }
    
    ///不動產減損流程
    public boolean Dero(){
        operator = new EstateDero();
        return operator.operate();
    }
}

那麼,假如我想要把不動產的新增流程,改成使用動產的新增流程,我只要改.....
    ///動產新增流程
    public boolean Add(){
        operator = new ProductAdd();   //這裡
        return operator.operate();
    }

一行就好了。


當然,你要把它做得更細緻一點的話,也可以再改一下.....
看到了嗎?利用這套模式,就能夠把程式變得彈性很夠,隨時能夠加以擴充,而不用修改到原本的程式。abstract和interface的混合應用,由這個例子可以看到,能夠把程式(或是系統架構)變得有彈性,程式之間的相依性變得低一點。
而這個,就是設計模式 (Design Patterns) 中,所謂的策略模式 (Strategy Pattern):
策略模式定義了演算法家族,個別封裝起來,讓它們之間可以互相替換,此模式讓演算法的變動,不會影響到使用演算法的程式。

設計模式沒有對與錯,只有有效或無效而已。
是否有效改善程式結構,讓軟體變軟。


----還有一些打破主題的廢話分隔線----

設計模式這種東西,其實是一些先賢烈士面臨到需求環境的多樣化後,加以設計,並且一般化而成的設計方法。當然,他們遇到的情境和我們將要面對的可能又不太一樣,若要直接套用,會是有風險在的。但是如果能夠把設計模式裡的想法和精髓融會貫通,情境、需求有變,就做出不同的設計就行了。

不過,約耳在他的書上表示:設計模式是一種讓程式設計生活從普通變得不錯、有趣的一種途徑,即使沒有它,程式一樣能動,只是可能維護上會變得比較麻煩(可能麻煩很多)、程式跑得慢一點…但是它還是能跑。
而低階一點的C/C++,如果指標沒弄好,出了差錯,程式會直接死在你面前,別想它會繼續往下走,你的程式設計生活就會很悲慘,所以做C/C++的程式員是非常辛苦的,可能會有抓不完的bug,和無法預期到的exception。但是在這種環境下訓練出來的程式員就特別強[5],邏輯能力強不強是超乎另一個次元的問題(被指標和遞迴洗禮過的人應該不會邏輯能力差吧?),程式員本身的韌性也夠。理解指標和遞迴的能力,與成為優秀程式員的能力有直接的關係。
所以,約耳覺得,在學校裡學Java、.NET這種高階語言,訓練出來的學生層次感不明顯,無法很立即能夠判斷哪些程式員是真的非常優秀非常聰明的。

Java和C#給程式員的幫助真的太多囉~它們就像是電腦一樣;而C/C++就像是紙張報表和筆。熟悉C/C++就會發現平常要想一天做半天的東西,用Java兩秒就解決了,而且還有很多容錯機制和回收機制,不用怕出錯。而使用過Java的人,要再回去寫C/C++…可能連字都不會寫吧。
我就快到了不會寫字的生活了。

ref:
[2] 深入淺出設計模式
[3] Program to an interface
[4] 約耳續談軟體

沒有留言: