2017年5月9日 星期二

無瑕的程式碼 – 類別

類別要夠簡短,比你想像中的還要更短。這裡說的簡短,並不單單只程式碼字數很少,它應該被解釋為「它該做的事情很少」,以下的方法可以幫助你來檢視你的類別是不是真的夠簡短了。



單一職責原則

單一職責原則是物件導向裡極為重要的概念,它主張一個類別或一個模組應該只能有一個修改的理由。假設,我們寫了一個叫做 TriangleShape 的類別:
        public class TriangleShape
        {
            public float m_fBase{ get; set; }
            public float m_fHeight{ get; set; }

            public float Area(){......}
            public void Draw(){......}
        }
    
這時,如果有人問起這個類別做了什麼事,而你的回答是「這個類別除了可以拿到三角形的底邊、高、面積之外,還可以有畫出三角形的功能呢!」當你發現描述類別的時候,出現了「還」這種字眼,其實就代表著你的類別已經超出一個以上的職責了。上面的例子還算可以接受,但如果你有一個類別超過三四十個方法,而且處裡的事情很多元,那可能會是一場災難的開始。

凝聚性

類別裡的實體變數應該要少少的,而且類別裡的方法應該要充分的利用它。當你開出的方法用到越多實體變數,代表這個方法更凝聚於該類別。也就是說,每個變數如果都被使用在每個方法中,你就會有一個超大凝聚力的類別。我們應該要盡可能設計高凝聚性的類別,讓他們結合成一個邏輯上的整體。
CH10_001
我們舉一個考慮高凝聚性時該注意的一個例子:當我們要為一個類別新增一個的方法(FunctionB)時,如果我們為此而新增很多實體變數(VariableD、VariableE),通常會造成類別的凝聚性變低。你應該要試著把它獨立出來(新增 ClassB)。

開放-閉合原則

系統的改變是持續性的,每一次的改變都讓我們承受一些風險。以下類別為資料庫串接的模組設計。記住,這個類別我很確定他還會在繼續擴增
        public class Sql
        {
            public Sql(string table);

            public string Create();

            public string Insert(Object[] fields);

            public string Find(string key);

        }
    
如果未來的某一天,我們需要為上面這個類別新增一個「Update」的方法,你會怎麼做?應該很直覺的,直接在類別最下方新增 Update Function 吧。但你能保證新增的方法不會影響到其他功能嗎?或者是,新增的這項功能還要去修改到別地方呢?這是都我們不樂見的,畢竟修改一個類別,你就必須承擔它可能會被改壞的風險。為了避免這樣的問題,我們可以把程式改寫成以下模式:
        public class Sql
        {
            public Sql(string table);
            public abstract string generate();
        }
        public class Create : Sql
        {
            public Create(string table);
            public override string generate();
        }
        public class Insert : Sql
        {
            public Insert(string table, Object[] fields);
            public override string generate();
        }
        public class Find : Sql
        {
            public Find(string table, string key);
            public override string generate();
        }
    
這樣的設計會變得很好擴充,如果我要新增 Update 功能,沒有任何一個已存在的類別需要被修改。我們只要把 Update 的程式邏輯寫在一個 Sql 的新子類別中就可以了。這就是物件導向設計的關鍵原則 – 開放閉合原則:我們要對類別的擴充具有開放性,但是對修改要有封閉性。

依賴反轉原則(Dependency inversion principle,DIP)

假設你今天要建立一個車子的類別,這個類別裡會用到「輪子」這個物件,你會怎麼寫?你應該會很直覺地寫出下面這段程式吧
        public class Wheel{...}
        public class Car
        {
            private Wheel[] m_wheel;
        }
    
我們在學物件導向的時候,會說 Car 這個類別有(Has a) Wheel 這個類別型態。也就是說,Car 是依賴 Wheel 這個類別。如果我們以模組的階層來看,Car 這個模組就是所謂的高層次模組,而 Wheel 就是在低層次的模組(就想像成長官和部屬的關係,高層長官可以命令低層員工做事。就像車子可以控制輪子一樣)。但這樣的設計並不完美。你可以設想一下,如果今天我們的輪子可以換成大輪子(LargeWheel)和小輪子(SmallWheel)的話,會發生什麼事?你會發現,又要為這兩種輪子各寫一個類別,而且這幾個類別其實根本大同小異,想必會有很多重複的程式在裡面。但這還不是最糟的,有沒有想過,你可能在組裝大輪子的時候,修改了一些車子本體的框架,結果導致本來可以裝一般輪子的車體框架,變得不能裝了,而且小輪子看似也裝不進去。正當你以為只要把大輪子塞好塞滿時,老闆竟跑來跟你說:「恩......我覺得這台車子應該三種輪子都要用上」
為了避免上述的這種窘境,設計模式就提出了一個叫做依賴反轉原則。他的定義如下
  • 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
  • 抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面。

以上面我們提到的例子來看,我們應該要建立一個 IWheel 的抽象類別,讓 Car 去依賴這個抽象類別。而且 IWheel 這個抽象類別不應該實作任何東西,讓繼承他的 LargeWheel 和 SmallWheel 去實作它。
        public interface IWheel{...}
        public class NormalWheel : IWheel{...}
        public class LargeWheel : IWheel{...}
        public class SmallWheel : IWheel{...}
        public class Car
        {
            private IWheel[] m_wheel = new IWheel[3];
            m_wheel[0] = new NormalWheel();
            m_wheel[1] = new LargeWheel();
            m_wheel[2] = new SmallWheel();
        }
    
我們在設計模組時,應該要先把抽象介面定義好,它該包含什麼變數和方法,先列出來。當我們要處裡高層次模組時,就要用已經定義好的抽象介面來實作,這時如果我們發現有少什麼功能,再加到抽象介面都還來的及。等到一切都連結好後,我們再來各別為低層次的模組來設計,當然,這時設計的低層次模組都要照著抽象介面的規則來,所以你不可能設計出裝不進車體的輪子了。這麼做不但增加了擴充的可能性,也隔離了我們可以修改的部分。不恰好也遵守了開放-閉合原則嗎?

參考資料

沒有留言:

張貼留言