設計模式系列:OOP設計6大原則

前言

相信有過開發經驗的人都有過這種體驗:讓你接手一個的項目,2種情況。A.這個項目已經被好幾個人,甚至好幾代程序員開發維護過;B.這個項目等待你的全新開發。不給你設時間期限,你更願意選擇哪一個?我相信99.9%的人都會選擇B這種開發模式。有木有??
Why?因爲不想改一個bug引起n個bug。說到底,就是因爲已有的項目架構沒有做好,或者沒有適時的做架構調整,假如你接手的是舊代碼,可能爲了添加一個功能,因爲架構不具備擴展性,你也許只能在原有的基礎上修改幾行代碼,甚至修改幾百上千行代碼來達到目的,以此來埋下諸多隱患待下一個接盤俠搞定。那麼就引出了今天的話題?什麼樣的代碼才具備可擴展性呢?

本文作者xiong_it,博客鏈接:http://blog.csdn.net/xiong_it。轉載請註明出處。

本篇文章已授權微信公衆號 guolin_blog(郭霖)獨家發佈.

Open Close Principle

OCP原則(開閉原則):一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。

wtf???太抽象了!!!在筆者的理解中,OCP是6大原則的最高綱領,所以才如此抽象,晦澀難懂。用面向對象的語言來講,OCP是一個最抽象的接口,而其餘的5大原則只是OCP的子類接口,他們一起定義了OOP世界的開發標準,常用的23中設計模式更是隻能算作這6大原則的實現抽象類,咱們開發的代碼實踐纔是真正的具體子類。

public interface OCP {
    void openExtention();
    void closeModifiability();
}

Q:What is OCP?
A:OCP是啥咧?它告訴我們,咱們編寫的代碼應該面向擴展開放,而儘量不要通過修改現有代碼來擁抱需求變更。這裏,代碼可以指的是一個功能模塊,類,或者方法。
Q:Why do we need to follow this principle?
A:我們爲什麼要遵循OCP原則呢?地球人都知道代碼後期需求變更的痛苦,如果不利用擴展來適應變更,那迎來的將是代碼被修改的千瘡百孔。
Q:How do we practice this principle?
A:我們如何實踐這條原則?能用抽象類的別用具體類,能用接口的別用抽象類。總之一句:儘量面向接口編程。這裏之所以說“儘量”是因爲凡事都有度,別讓你來個hello world你還給整個接口再實現。

talk is cheap,show your the code.

 需求:老王開車去東北。

簡單,開擼。

老王來了,大家藏好自己媳婦兒。

public class Laowang {
    private Car car;
    private DongBei dongbei;

    ...
    getter() & setter()
    ...

    public void drive() {
        car.goto(dongbei);
    }
}

要車就給你一輛咯

public class Car {
    public void goto(DongBei dongbei) {
        System.out.println(“要去東北咯,啦啦啦”);
        // 模擬開車旅途消耗時間。10s就到東北了,開的可夠快的啊!司機之前是開飛機的嗎?
        Thread.getCurrentThread().sleep(10 * 1000);
        System.out.println(“目的地東北到了”);
    }
}

東北到了

public class DongBei {
    private String address = "東北那旮沓兒";
}

老司機要發車了,趕緊打卡上車。滴,學生卡,咳咳咳,拿錯卡了。

public static void main(String[] args) {
    Car car = new Car();
    Laowang wang = new Laowang();
    wang.setCar(car);
    DongBei dongbei = new DongBei();
    wang.setDongbei(dongbei);
    wang.drive();
}

Perfect,完美!
現在需求變了,老王實現了2017年定下的小目標,掙了1個億,買了架私人飛機,他不想開車去東北了,太low,他要開飛機去東北。

需求2:老王開飛機去東北

簡單,給老王加個屬性,加幾個方法就實現了嘛?代碼就不擼了。
OK,又是一次完美變化!?
需求又變了,老張和老王是穿一條褲襠長大的發小,老張看老王這都開上飛機了,他的車是不是可以借來開一開?

需求3:老張開車去東北

這,這,這,簡單,重新擼一遍老王在需求1的代碼就行了,不就改個名的事嗎?
來來來,需求又變了,老張有急事去東北,老王就把飛機也借給老張用了。

需求4:老張開飛機去東北

這,這,這,這,這,這,簡單,把老王在需求2的代碼重擼一遍就是了。
來來來,需求又變了,老王這回不去東北了,他想開飛機去廣東那兒去探望下老丈人,順便兜兜風。

需求5:老王開飛機去廣東
需求6:老張開車去廣東
需求7:老王要開飛機去美國
需求8:小王要開車去西藏
需求...

這,這,這,這這這,R&D小哥一口老血噴在屏幕上,卒,享年25歲。

在這裏,筆者建議,將人物,交通工具,目的地抽象化,接口化,就可以適應需求的頻繁變更了。

上類圖
OCP demo類圖
客戶端代碼作如下調整:

public static void main(String[] args) {
    // 想用地上開的交通工具出行,好,那就new個車給你開
    ITransportation car = new Car();
    // 這次是老王要出門
    Person wang = new Laowang();
    // 老王選擇開車出行
    wang.setTransportation(car);
    // 老王目的地是東北
    AbsDestination dongbei = new DongBei();
    dongbei.setAddressName("東北");
    wang.setDestination(dongbei);
    // 老司機開着車就出發了
    wang.startOff();
}

老王的代碼如下

public class Laowang extends Person{
    ...
    public void startOff() {
        this.transportation.transport();
        System.out.println("出發咯");
        //thread.sleep();
        System.out.println("目的地" + this.destination.getAddressName() +"到了.");
    }
}

運行結果是:

出發咯
目的地東北到了.

現在,在需求的變更過程中,客戶端的代碼變化是不是小多了呢?

注意:開閉原則對擴展開放,對修改關閉,並不意味着不做任何修改,低層次模塊的變化,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。
在業務規則改變的情況下高層模塊必須有部分改變以適應新業務,改變要儘量地少,放置變化風險的擴散
—秦小波《設計模式之禪》

Single Responsibility Principle

SRP原則(職責單一原則):應該有且只有一個原因引起類的變更。

public interface SRP extends OCP {
    void onlyDoOneThing();
}

通俗點來講,一個類,一個方法只應該做一件事情。
舉2個栗子:
1.當一個類A有R1,R2兩個職責時,當R1的職責發生變更時,你需要修改類A,當R2發生變更時,你又需要修改類A,這時,已經有2個原因可能會引起類的變化了,類A就已經職責不單一了,就需要職責拆分,比如拆分成類A1,A2:A1類負責R1職責,A2類負責R2職責了。
2.再比如有一個方法M,它即負責計算和打印兩個職責

public void M(int a, int b) {
    int c = 0;
    c = a + b;

    System.out.println("打印的是 = " + c); 
}

有一天,你想要修改下計算規則,改爲

c=a+b+1;

此時,你修改了方法M。
又一天,你想修改下打印規則,改爲

System.out.println("打印的是 = " + (c+1)); 

你又修改了方法M,此時,超過了2個原因讓你去修改它,所以這個方法應該拆分爲待返回值得計算calc方法和打印print兩個方法。
似的每個方法都只做一件事情。

那它是如何體現擴展性的呢?
拿一個Android中最常見的ImageLoader的設計來舉例子,ImageLoader主要需要實現2個功能,下載圖片,緩存圖片。
假如,我們把所有的功能全部放在一個ImageLoader類中,假設下載要改方式呢?緩存要改策略呢?你通通要改ImageLoader,你如何保證修改某個功能的過程中另一個功能依舊完好,沒被污染?拆分職責,使用ImageCache接口及其子類實現進行緩存,和ImageLoader建立關聯,職責單一了,你再在每個單一的職責類裏面去修改相關代碼,這樣其他功能代碼被污染的概率大大降低。

當然,這裏只是隨意舉的例子,劃分單一職責這個度很難把握,每個人都需要根據自身情況和項目情況來進行判斷。

Liskov Substitution Principle

OCP原則(里氏替換原則):所有引用基類的地方必須能透明地使用其子類的對象

public interface LSP extends OCP {
    void liskovSubstitutionPrinciple();
}

通俗點講:只要父類能出現的地方子類就可以出現,而且替換爲子類也不產生任何異常錯誤,反之則不然。這主要體現在,我們經常使用抽象類/基類做爲方法參數,具體使用哪個子類作爲參數傳入進去,由調用者決定。

這條原則包含以下幾個方面:

  • 子類必須完全實現父類的方法
  • 子類可以有自己的個性外觀(屬性)和行爲(方法)
  • 覆蓋或者實現父類方法時,參數可以被放大。即父類的某個方法參數爲HashMap時,子類參數可以是HashMap,也可以是Map或者更大
  • 覆蓋或者實現父類的方法時,返回結果可以被縮小。即父類的某個方法返回類型是Map,子類可以是Map,也可以是HashMap或者更小。

Dependence Inversion Principle

DIP原則(依賴倒置原則):高層模塊不要依賴低層模塊,所以依賴都應該是抽象的,抽象不應該依賴於具體細節而,具體細節應該依賴於抽象

底層模塊:不可分割的原子邏輯就是低層模塊
高層模塊:低層模塊的組裝合成後就是高層模塊

抽象:Java中體現爲基類,抽象類,接口,而不單指抽象類
細節:體現爲子類,實現類

通俗點講,該原則包含以下幾點要素:

  • 模塊間的依賴應該通過抽象發生,具體實現類之間不應該建立依賴關係
  • 接口或者抽象類不依賴於實現類,否則就失去了抽象的意義
  • 實現類依賴於接口或者抽象類

總結起來,一句話:”面向接口編程“。

Interface-Segregation Principle

ISP原則(接口隔離原則):客戶端不應該依賴它不需要的接口;類間的依賴應該建立在最小的接口上

通俗點講:使用接口時應該建立單一接口,不要建立臃腫龐大的接口,儘量給調用者提供專門的接口,而非多功能接口。

這裏我想舉個例子就是Android中的事件處理Listener設計,大家都知道,我們想給button添加點擊事件時,可以使用如下代碼

button.setOnClickListener(clickListener);

想給它添加長按事件時,可以使用如下代碼

button.setOnLongClickListener(longClickListener);

還有其他比如OnTouchListener等等等事件接口,它爲什麼不直接提供一個通用的接口IListener呢?然後回調所有的事件給調用者處理,而要提供這麼多獨立的接口,這就是遵循了ISP原則的結果,每個接口最小化了,Activity/button作爲調用者,我可以選擇性的去處理我想處理的事件,不關心的事件Listener我就不去處理,依賴。

Low of Demeter

LoD法則(迪米特法則):又稱最少知識原則(Least Knowledge Principle, LKP),一個對象應該對其他對象有最少的瞭解。

通俗點講:一個類應該對自己需要耦合或者調用的類知道越少越好,被耦合或者調用的類內部和我沒有關係,我不需要的東西你就別public了吧。

迪米特法則包含以下幾點要素:

  • 只和朋友類交流:只耦合該耦合的類
  • 朋友間也是有距離的:減少不該public的方法,向外提供一個簡潔的訪問
  • 自家的方法就自己創建:只要該方法不會增加內部的負擔,也不會增加類間耦合

感謝和參考

秦小波:《設計模式之禪》
Mr.simple:《Android 源碼設計模式解析與實戰》
java-my-life:http://www.cnblogs.com/java-my-life/

後話

規則只是規則,大家不應該死守規則,應該持辯證的態度去看待這6大原則,才能更好地達到實踐應用的目的。
感謝以上作者和博客的規範化引導以及諸多博主的博客才漸漸讓我懂得實踐設計模式與應用架構。筆者將會未來陸續更新《設計模式系列》in Android博客,後續博客中,均參考了以上書籍和博客。歡迎各位朋友評論區點贊拍磚交流。
查看本文更新版本請點擊:https://xiong-it.github.io

後續:《Design Patterns in Android:目錄綱要》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章