菜鳥成長系列-面向對象的6種設計原則

菜鳥成長系列拖了一週多了,今天繼續以前的思路來進行。按照以前的規劃,這篇主要來學習設計原則先關知識。經過本文學習,但願你們一方面能是可以認識這些原則是什麼,可以在平常的開發中起到怎樣的約束,而且用這些原則來提升代碼的複用性和可維護性,另外一方面是對後續的設計模式的學習可以有一些基礎。java

菜鳥成長系列-概述
菜鳥成長系列-面向對象的四大基礎特性
菜鳥成長系列-多態、接口和抽象類
算法


設計原則,在java與模式這本書中有提到,用於提升系統可維護性的同時,也提升系統的可複用性。這本書中主要講了六種設計原則:編程

  • 「開-閉」原則
  • 里氏替換原則
  • 依賴倒置原則
  • 接口隔離原則
  • 單一職責原則
  • 迪特米法則

這些設計原則首先都是複用的原則,遵循這些原則能夠有效的提升系統的複用性,同時也提升了系統的可維護性。設計模式

「開-閉」原則

網上看到一我的的解釋,他是這樣來比喻的:一個本子,已經寫完了,你不可能撕幾張紙粘上去吧,最好的辦法是買個新的。
道理就是這樣,一個已經作好的程序,不支持修改的,由於修改的話,有可能形成程序沒法運行或報錯,因此,一般程序只支持擴展,不支持修改。bash

  • 1.爲何會有這樣一個原則來做爲程序設計的一種約束呢?
    在軟件的生命週期內,因爲軟件功能或者結構的變化、升級和維護等緣由須要對軟件原有代碼進行修改,在修改的過程當中可能會給舊代碼中引入錯誤,也可能會使咱們不得不對整個功能進行重構,而且還須要進行軟件的從新測試,所以咱們但願在軟件設計之初,可以用一種原則來進行一些基本的約束,使得在軟件後期的功能變動、擴展或者維護更加容易
  • 2.開閉原則解決的問題是什麼?
    當軟件須要進行改變時,咱們應該儘可能經過擴展軟件實體的行爲來實現變化,而不是經過修改已有的代碼來實現變化。經過這樣一種原則,能夠很好的實如今保證原有功能穩定的前提下擴展新的功能
  • 3.什麼是開閉原則呢?
    一個軟件實體(類、模塊或函數)應當對擴展開放,對修改關閉。也就是說在擴展或者修改軟件功能時,應儘可能在不修改原有代碼的狀況下進行

舉個簡單的栗子:如今有這樣一個需求,系統須要經過QQ來進行驗證登陸。OK,咱們來擼代碼:微信

  • 用戶類User
package com.glmapper.framerwork;
/**
 * 用戶信息類
 * @author glmapper
 * @date 2017年12月9日下午10:54:09
 *
 */
public class User {
	private String userName;//用戶名
	private String passWord;//密碼
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getPassWord() {
		return passWord;
	}
	public void setPassWord(String passWord) {
		this.passWord = passWord;
	}
}

複製代碼
  • QQ核心驗證邏輯
package com.glmapper.framerwork;
/**
 * QQ驗證器
 * @author glmapper
 * @date 2017年12月9日下午10:49:24
 */
public class QQAuther {
	/**
	 * 用於驗證QQ登陸信息
	 */
    public boolean validateQQ(User user)
    {
        //模擬下邏輯
        return user.toString()==null?false:true;
    }
}

複製代碼
  • 核心驗證服務類
package com.glmapper.framerwork;
/**
 * 
 * 用於驗證的核心服務
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 *
 */
public class AuthService {
	//持有一個QQ驗證器對象
	private QQAuther qqAuther;
	//經過構造器注入qqAuther對象
	public AuthService(QQAuther qqAuther) {
		this.qqAuther = qqAuther;
	}
	/*
	 * 驗證用戶合法性
	 */
	public boolean validateUser(User user){
		return qqAuther.validateQQ(user);
	}
}

複製代碼
  • 客戶端
package com.glmapper.framerwork;
/**
 * 客戶端調用驗證
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	
	public static void main(String[] args) {
		//獲取用戶信息
		User user = UserHolder.getUser();
		QQAuther qqAuther = new QQAuther();
		AuthService authService = new AuthService(qqAuther);
		//獲取驗證結果
		boolean isOK = authService.validateUser(user);
		System.out.println(isOK);
	}
}

複製代碼

OK,完事了!可是如今須要接入微博的開放平臺接口;修改代碼...。 增長一個微博驗證器:數據結構

package com.glmapper.framerwork;
/**
 * 微博核心驗證器
 * @author glmapper
 * @date 2017年12月9日下午11:01:10
 */
public class WeiBoAuther {
	/**
	 * 用於驗證QQ登陸信息
	 */
    public boolean validateWeiBo(User user)
    {
        return user.toString()==null?false:true;
    }
}

複製代碼

核心驗證服務修改:app

package com.glmapper.framerwork;
/**
 * 
 * 用於驗證的核心服務
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 *
 */
public class AuthService {
	//持有一個QQ驗證器對象
	private Object obj;
	//經過構造器注入qqAuther對象
	public AuthService(Object obj) {
		this.obj = obj;
	}
	/*
	 * 驗證用戶合法性
	 */
	public boolean validateUser(User user){
	    //這裏僅做爲模擬,通常狀況下會經過使用定義枚舉&工廠模式來完成
		if (obj instanceof QQAuther) {
			return new QQAuther().validateQQ(user);
		}
		if(obj instanceof WeiBoAuther){
			return new WeiBoAuther().validateWeiBo(user);
		}
		return false;
	}
}

複製代碼

客戶端改變:ide

package com.glmapper.framerwork;
/**
 * 客戶端調用驗證
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	
	public static void main(String[] args) {
		//獲取用戶信息
		User user = UserHolder.getUser();
		
		//QQ
		QQAuther qqAuther = new QQAuther();
		boolean isQQOK = new AuthService(qqAuther).validateUser(user);
		System.out.println(isQQOK);
		
		
		//微博
		WeiBoAuther weiBoAuther = new WeiBoAuther();
		boolean isWeiBoOK = new AuthService(weiBoAuther).validateUser(user);
		System.out.println(isWeiBoOK);
	}
}
複製代碼

OK,改進完成!可是又有新的需求,接入微信....。假如咱們如今把微信開放平臺也接入了,而後又來需求要接入支付寶帳戶、蘇寧易購帳戶等等。。。就須要不斷的修改代碼。那麼這個時候就須要在設計之初用到咱們的開閉原則來作一個約束了。繼續擼:
首先咱們須要須要定義一個接口用於約束:函數

  • 驗證器接口,用於被QQ/WEIBO/微信/蘇寧易購等開發平臺驗證器實現
package com.glmapper.framerwork;
/**
 * 定義一個約束接口 
 * @author glmapper
 * @date 2017年12月9日下午11:32:32
 *
 */
public interface ValidateInteface {
	/**
	 * 提供一個驗證入口
	 */
	boolean validate(User user);
}

複製代碼
  • QQ修改以後
package com.glmapper.framerwork;
/**
 * QQ驗證器
 * @author glmapper
 * @date 2017年12月9日下午10:49:24
 */
public class QQAuther implements ValidateInteface{
	/**
	 * 用於驗證QQ登陸信息
	 */
	@Override
	public boolean validate(User user) {
		return user.toString()==null?false:true;
	}
}

複製代碼
  • 微博修改以後
package com.glmapper.framerwork;
/**
 * 微博核心驗證器
 * @author glmapper
 * @date 2017年12月9日下午11:01:10
 */
public class WeiBoAuther implements ValidateInteface{
	/**
	 * 用於驗證QQ登陸信息
	 */
	@Override
	public boolean validate(User user) {
		// TODO Auto-generated method stub
		 return user.toString()==null?false:true;
	}
}
複製代碼
  • 核心驗證服務
package com.glmapper.framerwork;
/**
 * 用於驗證的核心服務
 * @author glmapper
 * @date 2017年12月9日下午10:47:04
 */
public class AuthService {
	//持有一個QQ驗證器對象
	private ValidateInteface validate;
	//經過構造器注入qqAuther對象
	public AuthService(ValidateInteface validate) {
		this.validate = validate;
	}
	/*
	 * 驗證用戶合法性
	 */
	public boolean validateUser(User user){
		return validate.validate(user);
	}
}

複製代碼
  • 客戶端
package com.glmapper.framerwork;
/**
 * 客戶端調用驗證
 * @author glmapper
 * @date 2017年12月9日下午10:50:13
 *
 */
public class AuthClient {
	public static void main(String[] args) {
		//獲取用戶信息
		User user = UserHolder.getUser();
		//QQ
		ValidateInteface qqAuther = new QQAuther();
		boolean isQQOK = new AuthService(qqAuther).validateUser(user);
		System.out.println(isQQOK);
		//微博
		ValidateInteface weiBoAuther = new WeiBoAuther();
		boolean isWeiBoOK = new AuthService(weiBoAuther).validateUser(user);
		System.out.println(isWeiBoOK);
	}
}

複製代碼

改進以後咱們能夠發現,對於原來的核心驗證服務類、各驗證器類,不管增長什麼方式接入,咱們都不須要去修改它的代碼了。而此時咱們須要作的就是新增一個驗證器(例如蘇寧易購驗證器),而後繼承ValidateInterface接口就好了。整體來首,開閉原則的核心是:

  • 抽象化
  • 對可變性的封裝原則(1.不可變性不該該散落在代碼的多處,而應當被封裝到一個對象裏面;2.一種可變性不該當與另一種可變性混合在一塊兒)

(你們若是有更簡單暴力的例子,能夠留言;這個例子想了不少都感受不是很恰當,仍是從工做中抽象出來的)。

里氏替換原則

任何父類能夠出現的地方,子類必定能夠出現
里氏替換原則算是對「開閉」原則的補充,上面也提到,實現「開閉」原則的關鍵步驟是抽象化,而父類與子類的繼承關係就是抽象化的一種具體體現,因此里氏替換原則是對實現抽象化的具體步驟的規範。

摘自java與模式中的定義:若是對每個類型爲 T1的對象 o1,都有類型爲 T2 的對象o2,使得以 T1定義的全部程序 P 在全部的對象 o1 都代換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型。

下圖中描述了一種繼承關係,從最高層的動物一直衍生出具體的動物。OK,寫一段斷碼來看看:

  • 頂層抽象父類-Animal
package com.glmapper.framework.model.lsp;
/**
 * 頂層抽象父類動物類
 * @author glmapper
 * @date 2017年12月10日上午10:51:30
 */
public abstract class Animal {
	//提供一個抽象方法,以供不一樣子類來進行具體的實現
	public abstract void eatFood(String foodName);
}
複製代碼
  • 具體動物類型-Dog
package com.glmapper.framework.model.lsp;
/**
 *子類-小狗
 * @author glmapper
 * @date 2017年12月10日上午10:54:17
 *
 */
public class Dog extends Animal{
	@Override
	public void eatFood(String foodName) {
		System.out.println("小狗吃"+foodName);
	}
}
複製代碼
  • 具體動物-哈士奇
package com.glmapper.framework.model.lsp;
/**
 * 具體小狗的種類-子類哈士奇
 * @author glmapper
 * @date 2017年12月10日上午10:56:59
 *
 */
public class HSQDog extends Dog{
	/**
	 * 重寫父類方法
	 */
	@Override
	public void eatFood(String foodName) {
		System.out.println("哈士奇吃"+foodName);
	}
}
複製代碼
  • 客戶端
package com.glmapper.framework.model.lsp;
//客戶端程序
public class ClientMain {
	public static void main(String[] args) {
		//子類
		HSQDog hsqdog=new HSQDog();
		hsqdog.eatFood("餅乾");
		//父類
		Dog dog = new HSQDog();
		dog.eatFood("餅乾");
		//頂層父類
		Animal animal = new HSQDog();
		animal.eatFood("餅乾");
	}
}
複製代碼
  • 運行結果
哈士奇吃餅乾
哈士奇吃餅乾
哈士奇吃餅乾
複製代碼

能夠看出咱們最開始說的那句話任何父類能夠出現的地方,子類必定能夠出現,反過來是不成立的。個人理解是子類經過集成獲取的父類的屬性和行爲,而且子類自身也具備本身的屬性和行爲;父類能夠出現的地方必然是須要用到父類的屬性或者行爲,而子類都涵蓋了父類的這些信息,所以能夠作到替換。反過來不行是由於父類在上述的例子中只是充當了一種類型約束,它可能不具備子類的某些特徵,所以就沒法作到真正的替換。

里氏替換原則是繼承複用的基石,只有當子類能夠替換掉基類,軟件單位的功能不會受到影響時,基類才能被真正的複用,而子類也纔可以在基類的基礎上增長新的功能。

依賴倒轉原則

實現「開閉」原則的關鍵是抽象化,而且從抽象化導出具體化實現。若是說開閉原則是面向對象設計的目標的話,依賴倒轉原則就是面向對象設計的主要機制(java與模式)。
依賴倒轉原則:要依賴與抽象,不依賴於具體實現。

怎麼理解呢?

  • 1)高層模塊不該該直接依賴於底層模塊的具體實現,而應該依賴於底層的抽象。換言之,模塊間的依賴是經過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是經過接口或抽象類產生的。

  • 2)接口和抽象類不該該依賴於實現類,而實現類依賴接口或抽象類。這一點其實不用多說,很好理解,「面向接口編程」思想正是這點的最好體現

首先是第一點,從複用的角度來講,高層次的模塊是設計者應當複用的。可是在傳統的過程性的設計中,複用卻側重於具體層次模塊的複用。好比算法的複用,數據結構的複用,函數庫的複用等,都不可避免是具體層次模塊裏面的複用。較高層次的結構依賴於較低層次的結構,而後較低層次的結構又依賴於更低層次的結構,直到依賴到每一行代碼爲止。而後對低層次修改也會逐層修改,一直到最高層的設計模塊中。

對於一個系統來講,通常抽象層次越高,它的穩定性就越好,所以也是做爲複用的重點

「倒轉」,實際上就是指複用應當將複用的重點放在抽象層上,若是抽象層次的模塊相對獨立於具體層次模塊的話,那麼抽象層次的模塊的複用即是相對較爲容易的了。

在不少狀況下,一個java程序須要引用一個對象,若是這個對象有一個抽象類型的話,應當使用這個抽象類型做爲變量的靜態類型。 在上面咱們畫了動物和小狗的類圖關係,在客戶端調用的時候有三種方式:

//子類(方式1)
HSQDog hsqdog=new HSQDog();
hsqdog.eatFood("餅乾");
//父類(方式2)
Dog dog = new HSQDog();
dog.eatFood("餅乾");
//頂層父類(方式3)
Animal animal = new HSQDog();
animal.eatFood("餅乾");
複製代碼

若是咱們須要一個哈士奇(HSQDog)的話,咱們不該當使用方式1,而是應當使用方式2或者方式3。

接口隔離原則

接口隔離原則:使用多個專門的接口比使用單一的總接口要好。換句話說,從一個客戶類的角度來說:一個類對另一個類的依賴性應當是創建在最小的接口上的。 這個其實在咱們實際的開發中是常常遇到的。好比咱們須要編寫一個完成一個產品的一些操做接口。

package com.glmapper.framework.model.isp;
/**
 * 一個產品服務接口
 * @author glmapper
 * @date 2017年12月10日下午12:01:31
 */
public interface ProductService {
	//增長產品
	public int addProduct(Product p);
	//刪除產產品
	public int deleteProduct(int pId);
	//修改產品
	public int updateProduct(Product p);
	//查詢一個產品
	public Product queryProduct(int pId);
}
複製代碼

OK,咱們在ProductService中提供了對產品的增刪改查;可是隨着需求升級,咱們須要能夠增長對產品新的批量導入和導出。OK,這時在接口中繼續新增兩個方法:

//從excel中批量導入
public void batchImportFromExcel();
//從excel中批量導導出
public void batchExportFromExcel();
複製代碼

而後需求又須要擴展,須要增長增長購買產品、產品訂單生產、查詢訂單、訂單詳情....;這樣一來,咱們的ProductService就會慢慢的急速膨脹。與此對應的具體的實現邏輯ProductServiceImpl類也會變得很是的龐大,可能單類會超過數千行代碼。

那麼咱們就須要進行接口隔離,將產品的基本操做如增刪改查放在一個接口,將產品訂單處理放在一個接口,將產品申購放在一個接口,將批量操做放在一個接口等等...對於每個接口咱們只關心某一類特定的職責,這個其實就是和單一職責原則有點掛鉤了。 經過這種設計,下降了單個接口的複雜度,使得接口的「內聚性」更高,「耦合性」更低。由此能夠看出接口隔離原則的必要性。

迪特米法則

迪特米法則:又稱爲最少知識原則,就是說一個對象應當對其餘對象儘量少的瞭解;看下迪特米法則的幾種表述:
1.只與你直接的朋友們通訊
2.不跟陌生人說話
3.每個軟件單位對其餘的單位都只有最少知識,並且侷限於那些與本單位密切相關的軟件單位

也就是說,若是兩個雷沒必要彼此直接通訊,那麼這兩個類就不該當發生直接的相互做用。若是其中一個類須要電泳另外一個類的某一個方法的話,能夠經過第三者進行消息的轉發。代碼看下:

  • 某我的
package com.glmapper.framework.model.isp;
/**
 * 某我的
 * @author glmapper
 * @date 2017年12月10日下午12:39:45
 */
public class SomeOne {
	//具體oprateion行爲
	public void oprateion(Friend friend){
		Stranger stranger =friend.provide();
		stranger.oprateion3();
	}
}
SomeOne具備一個oprateion方法,該方法接受Friend爲參數,根據上面的定義能夠知道Friend是SomeOne的「朋友」(直接通訊了)
複製代碼
  • 朋友
package com.glmapper.framework.model.isp;
/**
 * 朋友
 * @author glmapper
 * @date 2017年12月10日下午12:40:09
 */
public class Friend {
	private Stranger stranger = new Stranger();
	public Stranger provide(){
		return stranger;
	}
	public void opration2(){
	}
}
很明顯SomeOne的opration方法不知足迪特米法則,由於這個方法中涉及到了陌生人Stranger,Stranger不是SomeOne的朋友
複製代碼

OK,咱們來經過迪特米法則進行改造。

  • 改造以後的SomeOne
package com.glmapper.framework.model.isp;
/**
 * 某我的
 * @author glmapper
 * @date 2017年12月10日下午12:39:45
 *
 */
public class SomeOne {
	//具體oprateion行爲
	public void oprateion(Friend friend){
		friend.forward();
	}
}
複製代碼
  • 改造以後的朋友
package com.glmapper.framework.model.isp;
/**
 * 朋友
 * @author glmapper
 * @date 2017年12月10日下午12:40:09
 *
 */
public class Friend {
	private Stranger stranger = new Stranger();
	public void opration2(){
		
	}
	//進行轉發
	public void forward() {
		stranger.oprateion3();
	}
}
複製代碼

因爲調用了轉發,所以SomeOne中就不會和陌生人Stranger直接的關係就被忽略了。知足了直接和朋友通訊、不與陌生人說話的條件。
可是迪特米法則帶來的問題也是很明顯的:即會在系統中造出大量的小方法散落在系統的各個角落,這些方法僅僅是傳遞消息的調用,與系統的業務邏輯沒有任何關係。

單一職責

上面在接口隔離中有提到過,單一職責其實很好理解,解釋儘可能的使得咱們的每個類或者接口只完成本職工做之內的事情,不參與其餘任何邏輯。好比說蘋果榨汁機我就只用來榨蘋果汁,若是你須要榨黃瓜汁的話,你就得買一個黃瓜榨汁機。

總結

OK ,至此,設計原則部分就複習完了。總結一下:

    1. 單一職責原則要求實現類要職責單一;
    1. 里氏替換原則要求不要去破壞繼承系統;
    1. 依賴倒置原則要求面向接口編程;
    1. 接口隔離原則要求在設計接口的時候要精簡單一;
    1. 迪米特法則要求要下降耦合;
    1. 開閉原則是總綱,要求對擴展開發,對修改關閉。

你們週末愉快!(若是有不當之處,但願你們及時指出,多謝!)

相關文章
相關標籤/搜索