設計模式-六大原則

工欲善其事,必先利其器。 學習技術,貴“恆”,重“精”,忌“浮”。 切不可“這山望着那山高”
先“專心學精一門語言,然後對其它的語言便可融會貫通”

1. 六大原則

六大原則分別爲:單一職責,里氏替換原則,依賴倒置原則,接口隔離原則,迪米特法則,開閉原則.

單一職責原則 告訴我們實現類要職責單一;
里氏替換原則 告訴我們不要破壞繼承體系;
依賴倒置原則 告訴我們要面向接口編程;
接口隔離原則 告訴我們在設計接口的時候要精簡單一;
迪米特法則 告訴我們要降低耦合;
而開閉原則是總綱,開閉原則 告訴我們要對擴展開放,對修改關閉。
下面細細品味一下六大原則

1.1 單一職責(Single Responsibility Principle,簡稱是SRP)

就一個類而言,應該僅有一個引起它變化的原因;
把各個功能獨立出來,讓它們滿足單一職責原則;

舉個栗子:

// 可以看出來,這裏的接口設計的有問題,工作Work 和 生活Lift 沒有分開,這是一個嚴重的錯誤;
// 接口裏展示了兩個職責,工作 與 生活;
// 接口設計的一團糟,我們下面修改下;
public interface Human {
    public void work相關1();
    public void work相關2();
    public void work相關3();
    public void work相關4();
    ... ...
    
    public void life相關1();
    public void life相關2();
    public void life相關3();
    public void life相關4();
    ... ...
}

修改後的栗子,將 Human 的 工作 抽取爲 Work,生活抽取爲 Life;
在這裏插入圖片描述

當然除了在類中,函數的定義也可以儘量的單一職責,顆粒度多細,取決於實際的場景;
比如 初始化數據 initAllDatas,初始化界面 initAllViews 等等

// Activity onCreate demo 處理演示
 @Override    
 protected void onCreate(Bundle savedInstanceState) {        		  
 	super.onCreate(savedInstanceState);        
 	setContentView(R.layout.activity_login);        
	loginTimes = -1;        
 	Bundle bundle = getIntent().getExtras();
    String strEmail = bundle.getString(AppConstants.Email);        
    etEmail = (EditText) f indViewById(R.id.email);        		
    etEmail.setText(strEmail);        
    etPassword = (EditText) f indViewById(R.id.password); // 登錄事件        
    Button btnLogin = (Button) findViewById(R.id.sign_in_button);        
    btnLogin.setOnClickListener(this);        
    // 獲取2個MobileAPI,獲取天氣數據,獲取城市數據        
    loadWeatherData();        
    loadCityData();    
} 

// 更改後的代碼效果
 @Override    
 public void onCreate(Bundle savedInstanceState) {        
 	super.onCreate(savedInstanceState);                
 	initVariables();        
 	initViews(savedInstanceState);        
 	loadData();    
}

注意:
單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否有優良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環境而異。

避免設計過度:
導致於類/方法/接口過多,反而不好管理,當然也要避免違背。

遵循單一職責原的優點:

  1. 提高類的可維護性,可讀寫性;可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;代碼少了,不復雜,可讀性也就好了,哈哈哈。

  2. 提高系統的可維護性;系統由類組成的,每個類的可維護性高,相對來講整個系統的可維護性就高。可讀性提高了,當然就容易維護了,嘿嘿。

  3. 降低變更的風險,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。一個類的職責越多,變更的可能性就更大,變更帶來的風險也就越大,

1.2 開閉原則(Open Closed Principle,簡稱是OCP)

用抽象構建框架,用實現擴展細節(抽象化是開閉原則的關鍵)
開閉原則的核心是:對擴展開放,對修改關閉

舉個栗子:

// 錯誤的示範,緩存的栗子
public class OpenCloseTest {
    // 這裏存在的問題就是,如果有一天又增加其它緩存方式,
    // 又要修改 OpenCloseTest 的loadImage代碼
    // 整個loadImage 不僅越來越複雜,脆弱,也會越來越臃腫,可擴展性也很差
    // 這裏就違背了開閉原則,擴展不是開放的,修改不是封閉的
    public void loadImage(String type) {
        if ("內存緩存".equals(type)) { // 內存緩存
           ... ...
        } else if ("硬盤緩存".equals(type)) { // 硬盤緩存
           ... ...
        } else { // 雙緩存
           ... ...
        }
    }
}

// 修改後的栗子 1. 增加抽象

public interface ICache {
	public Bitmap get(String url);
}

//  內存緩存
public class MemoryCache implements ICache {
}
// 硬盤緩存
public class DiskCache implements ICache {
}
// ... ...

public class OpenCloseTest {
    // 外部傳入各種緩存,可擴展性,靈活性高;
    // 符合開閉原則,擴展是開放的,修改是封閉的
    public void loadImage(ICache cache) {
        Bitmap b = cache.get("http://test");
    }
}

注意:
開閉原則指導我們,當軟件需要變化時,應該儘量通過擴展的方式來實現變化,而不是通過修改己有的代碼來實現。
這裏的“應該儘量”4個字說明 OCP 原則並不是說絕對不可以修改原始類的。
當我們嗅到原來的代碼“腐化氣味”時,應該儘早地重構,以便使代碼恢復到正常的“進化”過程,而不是通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗餘。

避免設計過度:
切忌到處都抽象,會導致系統過度設計,過度複雜。這反而是不利於系統的維護。完全的開閉原則是不可能實現的,所以請保持簡單設計,在需要的時候做符合開閉原則的設計。

1.3 里氏替換原則(Liskov Substitution Principle,簡稱LSP)

子類可以擴展父類的功能,但不能改變父類原有的功能;
說白了 里氏替換原則其實就是爲“良好的繼承”制定一些規範;
里氏替換原則是實現開閉原則的重要方式之一;

注意幾點:

  • (關鍵點)子類可以實現父類的抽象方法,但儘量不要覆蓋父類的非抽象方法

  • 子類中可以增加自己特有的方法

  • 子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。(比如: 父類參數 T,子類 參數 S,要求 S 大於 T。要麼同一個類型,要麼 T 是 S 的子類;比如 父類參數 HashMap,子類就是 Map)

  • 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。(比如:父類 返回值爲類型T,子類返回值爲S,要求S必須小於等於T。要麼 S 和 T 是同一個類型,要麼 S 是 T 的子類;如父類要求返回List,那麼子類就應該返回List的實現ArrayList,父類是採用泛型,那麼子類則不能採用泛型,而是具體的返回)

舉個栗子:

public interfaceclass I {
    public List testMapRes2();
}

 // 父類
public class A implements I {
	public int add(int num1, int num2) {
		return num1 + num2;
	}
	// 父類 輸入參數是 HashMap 類型
	public String testMap(HashMap map) {
		return "父類 A testMap 函數 測試>>>";
	}
	// 實現父類的抽象方法
	@Override
    public ArrayList testMapRes2() { // 方法的返回值 ArrayList 比父類的更嚴格
        return null;
    }
}

// 子類 繼承 父類A,覆蓋了父類的非抽象方法
public class B extends A { 
    // 錯誤的方法!!!
    @Override
	public int add(int num1, int num2) {
		return num1 - num2;
	}
	// 子類的輸入參數是 Map 類型
	// 子類的輸入參數類型的範圍擴大了
	// @Override 是失效的,因爲現在是 重載(Overload)
	public String  testMap(Map map/*子類的形參比父類的更寬鬆*/) {
	    return "子類 B testMap 函數 測試<<<";
	}
	// 添加自己的特有的方法
	... ...
}

... ...
public static void main(... ...
	B b = new B();
	// 結果爲5,覆蓋重寫父類的非抽象方法,導致計算結果有誤
	// 可以實現父類的抽象方法,但是不要覆蓋父類的非抽象方法.
	b.add(10, 5); 
	// 結果爲 "父類 A testMap 函數 測試>>>"  OK,正確的.
	// 如果反過來,父類 Map,子類HashMap,子類被執行,可能會導致業務邏輯混亂,因爲父類實現好的方法,一般是繼承使用就好,.
	// 所以子類中方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆
	HashMap map = new HashMap();
    b.testMap(map);
    //  實現I的抽象方法
    A a = new A();
    a.testMapRes2();

注意:

  1. 父類設計爲抽象或者接口,讓子類繼續父類或實現父接口,並實現在父類中聲明的方法;
  2. 儘量避免子類重寫父類的非抽象方法,可以有效降低代碼出錯的可能性;

繼承的優缺點:
優點:
1. 提高代碼的重用性,子類擁有父類的方法和屬性;
2. 提高代碼的可擴展性,子類可形似於父類,但異於父類,保留自我的特性;

缺點:
1. 繼承是侵入性的,只要繼承就必須擁有父類的所有方法和屬性,在一定程度上約束了子類,降低了代碼的靈活性;
2. 增加了耦合,當父類的常量、變量或者方法被修改了,需要考慮子類的修改,所以一旦父類有了變動,很可能會造成非常糟糕的結果,要重構大量的代碼。

比較通用的做法是(只是建議):
原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。

遵守里氏替換原則,在一定程度上增強程序的健壯性

1.4 依賴倒置原則(Dependence Inversion Principle,簡稱DIP)

高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

依賴倒置原則的核心思想是面向接口編程,主要是實現解耦;

  • 高層模塊不應該依賴底層模塊(具體實現細節),二者都應該依賴其抽象(抽象類或接口)
  • 抽象不應該依賴細節
  • 細節應該依賴於抽象

這麼去理解上面的這幾句話?
抽象:Java代碼中,抽象就是指 接口(interface) 或者 抽象類(abstract)
高層模塊:負責複雜的業務邏輯
底層模塊:具體實現細節,

舉個栗子
問題由來:類A直接依賴類B,假如要將類A改爲依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責複雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。


class B { // 類B和類C是低層模塊,負責基本的原子操作
	public String getTestContent() {
		return "獲取A測試內容";
	}
}

class C { // 類B和類C是低層模塊,負責基本的原子操作
	public String getTestContent() {
		return "獲取C測試內容";
	}
}

// 類A直接依賴類B 
class A { // 類A一般是高層模塊,負責複雜的業務邏輯
	// 假如要將類A改爲依賴類C,則必須通過修改類A的代碼來達成。
	// 假如修改類A,會給程序帶來不必要的風險。
	public void test(B b) {
		System.out.println("===依賴倒置測試===");
		System.out.println(b.getTestContent());
	}
}
 
public class Client {
	public static void main(String[] args) {
		A aa = new A();
		aa.test(new B());
	}
}	

解決方案:將類A修改爲依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯繫,則會大大降低修改類A的機率

interface I {
	public String getTestContent();
}

class B implements I {  // 類B和類C各自實現接口I
	@Override
	public String getTestContent() {
		return "獲取A測試內容";
	}
}

class C implements I {  // 類B和類C各自實現接口I
... ...

// 類A直接依賴類B 
class A { // 類A一般是高層模塊,負責複雜的業務邏輯
	// 類A通過接口I間接與類B或者類C發生聯繫,
	// 則會大大降低修改類A的機率
	public void test(I i) {
		System.out.println("===依賴倒置測試===");
		System.out.println(i.getTestContent());
	}
}


public class Client{
	public static void main(String[] args){
		A a = new A();
		a.test(new A());
		a.test(new C());
	}
}

注意:
低層模塊儘量都要有抽象類或接口,或者兩者都有。
變量的聲明類型儘量是抽象類或接口。
使用繼承時遵循里氏替換原則。

1.5 接口隔離原則(Interface Segregation Principle,簡稱ISP)

類間的依賴關係應該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。
接口隔離原則的目的是系統解開耦合,從而容易重構、更改和重新部署。
客戶端不應該依賴它不需要的接口。

Bob大叔(Robert C Martin)在21世紀早期將單一職責、開閉原則、里氏替換、接口隔離以 及依賴倒置(也稱爲依賴反轉) 5個原則定義爲SOLID 原則,作爲面向對象編程的5個基本原則。

舉個栗子:

// 錯誤示範,比如 定義了 改變世界 的接口
public interface 改變世界 {
    void 寫代碼();
    void 代碼審查();
    void APK提測();
    void APK發佈();
}
// 小公司 XXOO 剛開始的時候,一步到位
// 程序員A 實現了所有的方法,沒辦法,小公司,身兼多職
public 程序員A implements Travel {
}

'隨着公司發展壯大,流程越來越規範'
依賴它不需要的接口,只是極少實現了部分接口的方法;
'程序員-A'   發現它是需要 寫代碼 就可以了;
'某Leader-B' 只是 代碼審查;
'測試同學-C' 只是 APK測試;
'運營同學-D' 只是 APK發佈;

這就導致接口隔離不是很少,其它人還是實現了多餘的3個接口,這麼辦?
修改後:

interface 程序員相關 {
	void 寫代碼();
	... ...
}
interface 運營相關 {
	void APK發佈();
	... ...
}
... ...
這樣對應的人,只需要實現對應的接口就OK了

注意:
根據實際情況,具體業務具體分析,合理使用接口隔離原則;
在特定的場景下,如果很多類實現了同一個接口,並且都只實現了接口的極少部分方法,這時候很有可能就是接口隔離性不好,就要去分析能不能把方法拆分到不同的接口。

避免設計過度:
設計接口的粒度越小,系統越靈活是肯定的。但是過度把接口設計粒度鎖到很小,這樣會增加系統閱讀代碼的複雜度。接口設計儘量使其能完成一個特有的功能,而不能把一個功能再進行拆分,拆分出好多接口來,這就過度設計了,度很難把握。

1.6 迪米特法則(Law Of Demeter,簡稱LOD)

也稱爲最少知識原則(Least Knowledge Principle),
通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,類的內部如何實現與調用者或者依賴者沒關係,調用者或者依賴者只需要知道它需要的方法即可,其他的可一概不用管。
類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
少暴露細節.

舉個栗子:

// 女朋友 讓你給 她 數星星
public class GirlFriend {
	// GirlFriend 類 除了 和 My 這個朋友類 有交流,還和 Star 有了交流;
	// 迪米特法則說是一個類只和朋友類交流,這裏是不對的!!!
	public void commond(My my) {
		List<Star> starList = new ArrayList();
		for (int i=0;i<1000;i++) {
			starList.add(new Star())
		}
		my.數星星(starList)
	}
}

// 修改後的例子
// GirlFriend 避開了 對陌生類 star 的訪問,減少耦合;
public class GirlFriend {
	public void commond(My my) {
		my.數星星()
	}
}

注意:
記住,類只和朋友交流,不要和陌生類交流,有危險,媽媽說的;

避免設計過度:
過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統複雜度變大。

迪米特法則 的優點:
類之間解耦,弱耦合。

1.7 總結

寫的代碼 應該 保持 高可擴展性、高內聚、低耦合;
在經歷了各版本的變更之後依然保持清晰、靈活、穩定的系統架構。
雖然 實際情況下,會因爲時間,項目需求經常變更,場景的各種原因,但是我們還是朝這個方向努力,努力遵守六大原則 就是 讓代碼更穩定,靈活,清晰的 第一步。

六大原則是一把“雙刃劍”,使用得當是一把神兵利器,使用過度那麼就是自殺行爲。
在這裏插入圖片描述
遵守的差,點將會在小圓內部;
過渡遵守,點將會落在大圓外部;
設計1、設計2屬於良好的設計;
設計3、設計4設計雖然有些不足,但也基本可以接受;
設計5則嚴重不足,對各項原則都沒有很好的遵守;
設計6則遵守過渡了,
設計5和設計6都是迫切需要重構的設計。
在這裏插入圖片描述

2. 各種設計模式瞭解(下個章節講解部分常用的)

  • 創建型
  1. Factory Method(工廠方法)
  2. Abstract Factory(抽象工廠)
  3. Builder(建造者)
  4. Prototype(原型)
  5. Singleton(單例)
  • 結構型
  1. Adapter Class/Object(適配器)
  2. Bridge (橋接)
  3. Composite(組合)
  4. Decorator(裝飾) Android源碼中的ContextWrapper
  5. Facade(外觀)
  6. Flyweight(享元)
  7. Proxy(代理)
  • 行爲型
  1. Interpreter(解釋器)
  2. Template Method(模板方法)
  3. Chain of Responsibility(責任鏈) Android的事件分發機制
  4. Command(命令)
  5. Iterator(迭代器)
  6. Mediator(中介者)
  7. Memento(備忘錄) 類似遊戲的保存與恢復
  8. Observer(觀察者)
  9. Iterator(迭代器)
  10. Strategy(策略)
  11. Visitor(訪問者)

3. 新知識擴展

對象池模式
規格模式
僱工模式
黑板模式
空對象模式

4. 推薦資料

《設計模式之禪》,《大話設計模式》,《Android 源碼設計模式解析與實戰》,《圖解設計模式》

多閱讀下別人的源碼,比如 Android 的同學,可以看看 Glide,Retrofit。

http://linbinghe.com/2017/1646c9f3.html 談談23種設計模式在Android源碼及項目中的應用

http://blog.csdn.net/zhengzhb/article/details/7296944 設計模式六大原則(6):開閉原則

http://www.cnblogs.com/yanghuahui/p/3308487.html 用枚舉實現工廠方法模式更簡潔?

https://zhuanlan.zhihu.com/p/33607390 快速理解 設計模式六大原則

https://wenku.baidu.com/view/59684b4326d3240c844769eae009581b6bd9bd15.html 里氏替換原則

發佈了37 篇原創文章 · 獲贊 18 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章