1. 程式人生 > >解讀設計原則

解讀設計原則

場景 ood 子類 寫代碼 oschina tro 也會 客戶端 選擇

概述

設計原則就一本菜譜,告訴我們一道美味的菜應該是什麽樣的,或者說需要具備什麽。但是又沒有一個固化或可測量的標準。寫代碼就和烹飪一樣,只有當自己品嘗以後才知其味。

1 開閉原則

定義:

開閉原則(Open-Closed Principle, OCP):一個軟件實體應當對擴展開放,對修改關閉。即軟件實體應盡量在不修改原有代碼的情況下進行擴展。

解讀

開閉原則很簡單,就是當需求變更的時候,盡量不要修改已有代碼。開閉原則是整個設計原則的核心思想。如果當你發現某個需求的改動需要涉及許多地方的代碼改動,那麽你的代碼很有可能是不滿足開閉原則的。

小結

單獨說開閉原則是沒有什麽內容可講的,就像和你說“堅強的人可以克服任何困難”。那麽其實我們關心的是怎麽才能變得“堅強”。其他的設計原則其實都是圍繞著怎麽實現開閉原則而進行的。

2 單一職責原則

定義:

單一職責原則(Single Responsibility Principle, SRP):一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。

解讀

通俗一點說,就是一個類不能承擔太多的功能。只專心一件事,並且把這件事做好。舉一個例子,假如要寫一個食堂類。對於客戶端來說,食堂核心功能是要能打飯和回收餐具。如果要打飯那麽肯定需要有先把菜做好,如果要做菜還要有人去買菜。那麽如果這些功能都放在一個類裏面就會如下圖:
技術分享 很明顯違反了單一職責原則。為什麽?明明就是只負責吃相關職能啊。你都說了是吃相關,那麽肯定就不只是一個簡單功能。吃前要準備,吃後要收拾。其實作為一個服務,就和我們去食堂吃飯,我只關心怎麽打飯,我吃完以後把餐具放哪裏。其他的至於菜市從哪裏買的,誰炒的都不關心。因此,改良一下
技術分享


作為食堂,提供多種多樣的菜品(如川菜、粵菜)。如果讓一個廚師即炒粵菜又炒川菜又違背了單一職責原則。因此再改一下:
技術分享

單一職責好處就是: 可以降低類的復雜度,提高類的可讀性,降低變更引起的風險降低。變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。

小結

其實單一職責不僅僅是針對一個類的設計,往小的說一個方法、往大的說一個模塊都應該滿足單一職責原則。只是怎麽去確定職責及其範圍是需要根據具體的場景來確定。

3 裏氏代換原則

定義:

裏氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

解讀

裏氏代換原則核心就是要滿足繼承體系,能用父類的地方,那麽其任何子類也可以正確調用。否則,就不應該做為其子類(或者父類的定義就不正確)。例如鳥是一個父類,那麽所有的鳥都應該是卵生。但是如果父類定義了方法fly()就不一定了,因為有的鳥不會飛的。這個就不滿足裏氏替換原則。從代碼層面上來說:如果一個類實現一個接口,那麽就應該實現該接口的所有方法。比較經典的案例就是Spring的CacheManager的接口。

小結

如果你理解(或者使用過)面向對象語言,那麽裏氏替換原則理解起來就十分簡單。例如java在編譯的時候就會檢查一個程序是否符合裏氏替換原則。雖然很簡單,但是這也是實現開閉原則的基本條件。試想,當擴展一個接口的時候,發現擴展類在實現該接口的方法不能正確調用,那麽這個該擴展也是沒有任何意義的。

4 依賴倒轉原則

定義:

依賴倒轉原則(Dependency Inversion Principle, DIP):抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對接口編程,而不是針對實現編程。

解讀

上面定義中的抽象可以簡單的理解為接口,細節就是接口的實現類。也就是說,無論是定義變量、方法參數類型還是方法返回都盡量使用抽象接口,而不是返回具體的實現類。這樣做的目的就是方便以後擴展(也就是實現開閉原則的手段)。還是前面的食堂為例,假如一開始的時候只有MarketA賣菜,采購員就只有去哪裏賣菜。也許一開始的代碼就會這樣寫:

class MarketA {
    public String name() {
        return ("MarketA");
    }
}

class Buyer {
    public void buy(MarketA marketA) {
        System.out.println("采購員在" + marketA.name() + "買菜");
    }
}

public void CanteenBuyFood() {
    Buyer buyer = new Buyer();
    buyer.buy(new MarketA());
}

  

突然有一天,MarketB開業,並且每周一的價格比MarketA便宜,因此,選擇周一在MarketB買菜。

class MarketA {
    public String name() {
        return ("MarketA");
    }
}


class MarketB {
    public String name() {
        return ("MarketB");
    }
}

class Buyer {
    public void buyA(MarketA marketA) {
        System.out.println("采購員在" + marketA.name() + "買菜");
    }

    public void buyB(MarketB marketB) {
        System.out.println("采購員在" + marketB.name() + "買菜");
    }
}

public void CanteenBuyFood() {
    Buyer buyer = new Buyer();
    // 其實這也也不滿足單一職責原則,即要選擇菜市場,又要派采購員買菜。
    if(to is monday){
        buyer.buyA(new MarketA());
    }else{

        buyer.buyB(new MarketB());
    }
}

  

突然又有一天,MarketC,MarketD...MarketCX 開業了,並且....好吧,是不是發現這樣寫下去什麽時候是個頭啊。因此我們需要進行代碼重構,使其滿足開閉原則。

interface Market {
    String name();
}

class MarketA implements Market {
    public String name() {
        return ("MarketA");
    }
}


class MarketB implements Market {
    public String name() {
        return ("MarketB");
    }
}

interface Buyer {
    public void buy(Market market);
}
//省略Buyer的子類


public void CanteenBuyFood() {
    Market market = selectMarket();
    Buyer buyer = selectBuyer();
    buyer.buy(market);
}

private Buyer selectBuyer() {
    //根據實際情況選擇采購員
}

private Market selectMarket() {
    // 根據情況選擇市場  
}

  

重構後代碼是不是無論加多少market,只要修改selectMarket()的方法。無論加多少采購員,只要修改Buyer.getBuyer()中的代碼。

小結

可以看出,在代碼重構過程中,在大多數情況下開閉原則、裏氏代換原則和依賴倒轉原則這三個設計原則會同時出現。開閉原則是目標,裏氏代換原則是基礎,依賴倒轉原則是手段,他們目的都是為了代碼的擴展性,只是分析問題時所站角度不同而已。

5 接口隔離原則

定義:

接口隔離原則(Interface Segregation Principle, ISP):使用多個專門的接口,而不使用單一的總接口,即客戶端不應該依賴那些它不需要的接口。

解讀

接口隔離就是定義一個接口的時候不要定義得太而廣,而是把他們分割成一些更細小的接口。一般不滿足單一職責原則都不滿足接口隔離原則,這樣的例子很多,就不多說明了。但反過來卻不一定,舉一個例子(往往原則的例子舉反例是最清晰的)。還是買菜的問題,如果我們定義一個市場,在市場裏面買肉、買蔬菜、買水果....),往往一開始我們定義接口的時候是按照一個樣例來定義接口,比如上面就是按照一個超級市場來定義的接口,其實這樣做是很正常的情況。

interface Market {
    void buyMeat();

    void buyVegetables();
}

class SuperMarket implements Market {

    public void buyMeat() {
        System.out.println("買肉");
    }

    public void buyVegetables() {
        System.out.println("買菜");
    }
}

  

這樣定義接口在一開始是沒問題,但是隨著業務擴展,如果現在存在另外一個市場,他只賣菜,不賣肉。。那麽該類在 buyMeat()下面怎麽辦?當你發現你實現一個接口的時候,某個需要實現的方法你沒辦法去實現,這種情況也算不滿足接口隔離的原則了。這時候就需要進行重構,把接口進一步細化,例如,把Martet細化為MeatMarket和VegetableMarket。

interface Market {
    //其他共有方法
}

interface MeatMarket extends Market{
    void buyMeat();
}

interface VegetableMarket extends Market{
    void buyVegetables();
}

class SuperMarket implements MeatMarket,VegetableMarket {

    public void buyMeat() {
        System.out.println("買肉");
    }

    public void buyVegetables() {
        System.out.println("買菜");
    }
}

class SmallMarket implements VegetableMarket {

    public void buyVegetables() {
        System.out.println("買菜");
    }
}

  

上面的實例代碼僅僅是為了說明接口隔離原則,

小節

接口隔離原則核心思想就是細化接口,提高程序的靈活性。但細化到什麽程度卻沒有具體的度量,接口不能太小,如果太小會導致系統中接口泛濫,反而不利於維護。因此如何把握這個讀就是經驗了。

迪米特法則

定義

迪米特法則(Law of Demeter, LoD):一個軟件實體應當盡可能少地與其他實體發生相互作用。

解讀

迪米特法則還有幾種定義形式,包括:不要和“陌生人”說話、只與你的直接朋友通信等

一個類只與他的朋友類進行交互

所謂一個類的朋友,就是該類

  1. 當前對象本身(this);
  2. 以參數形式傳入到當前對象方法中的對象,或者返回的方法返回的對象
  3. 當前對象的成員對象;

從代碼層面上講,當該類的所有方法體為空的時候,該類所依賴的類就是朋友類 所謂只與他朋友類進行交互意思就比較好理解了,就是不能直接調用他朋友的朋友方法。也就是在技術分享中,食堂服務\=\=朋友==>食堂後勤\=\=朋友==>廚師,我們不能直接在食堂服務中直接調用讓廚師去炒菜。食堂服務類要與廚師類通信,也是必須要有食堂後勤類作為中介。從代碼層面上來說,一般 A.getB().getC().xxxMethod() 往往都是破壞迪米特法則。

即使是朋友也要保持距離

一個類公開的public屬性或方法越多,修改時涉及的面也就越大,變更引起的風險擴散也就越大。例如,

class A {
    public void step1(){
        //do something
    }

    public void step2(){
        //do something}
    }

    public void step3(){
        //do something}
    }
}
class M {
    public void someCall(A a) {
        a.step1();
        a.step2();
        a.step3();
    }
}

  

從代碼裏面看,A確實是M的朋友,但是,M和A太"親密"了,如果以後需要在調用step2的之前調用一個check2()的方法,就必須要修改M中的方法,如果這個調用方式大量出現在工程中,就會引起擴散。如果我之前是這麽設計的交互的方式就不會存在這個問題。也就是說,朋友之間的交互不要太"親密"。同時作為別人朋友,最好能提供“一站式服務”。

class A {
    private void step1(){
        //do something
    }

    private void step2(){
        //do something}
    }

    private void step3(){
        //do something}
    }

    public void exe(){
        step1();
        step2();
        step3();
    }
}
class M {
    public void someCall(A a) {
       a.exe();
    }
}

  

小節

迪米特發展的目的就是降低系統的耦合度,使類與類之間保持松散的耦合關系。和接口隔離原則一樣,迪米特法則也是一個無法進行度量。過度使用迪米特法則,也會造成系統的不同模塊之間的通信效率降低,這就直接導致了系統中存在大量的中介類。因此,如何使用迪米特法則還是那句話,根據經驗。

總結

單一職責原則告訴我們實現類的功能不要太多。裏氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則總的大綱,要對擴展開放,對修改關閉。
設計原則只是給我們一些指導性的意見,在實際工作中往往要根據實際情況來判斷。就如開篇所說的那樣,往往菜譜往往給我們的意見都是“少許”,“適量”,而真正要烹飪出一道美味還需要我們自己不斷去積累和調整。

解讀設計原則