設計模式六大原則(3):依賴倒置原則


 

依賴倒置原則

 

設計模式系列文章

1、問題由來

  類A直接依賴於類B,假如要將類A修改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯。類B和C是底層模塊,負責基本的原子操作。假如修改類A,將會給程序帶來不必要的風險。而遵循依賴倒置原則的程序設計可以解決這一問題。

2、什么是依賴倒置原則

  英文縮寫DIP(Dependence Inversion Principle)。

  原始定義:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

  翻譯過來就三層含義:

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

  抽象:即抽象類或接口,兩者是不能夠實例化的。

  細節:即具體的實現類,實現接口或者繼承抽象類所產生的類,兩者可以通過關鍵字new直接被實例化。

  現在我們來通過實例還原開篇問題的場景,以便更好的來理解。下面代碼描述了一個簡單的場景,Jim作為人有吃的方法,蘋果有取得自己名字的方法,然后實現Jim去吃蘋果。

  代碼如下:

//具體Jim人類
public class Jim {
	public void eat(Apple apple){
		System.out.println("Jim eat " + apple.getName());
	}
}
//具體蘋果類
public class Apple {
	public String getName(){
		return "apple";
	}
}
public class Client {
	public static void main(String[] args) {
		Jim jim = new Jim();
		Apple apple = new Apple();
		jim.eat(apple);
	}
}

  運行結果:Jim eat apple

  上面代碼看起來比較簡單,但其實是一個非常脆弱的設計。現在Jim可以吃蘋果了,但是不能只吃蘋果而不吃別的水果啊,這樣下去肯定會造成營養失衡。現在想讓Jim吃香蕉了(好像香蕉里含鉀元素比較多,吃點比較有益),突然發現Jim是吃不了香蕉的,那怎么辦呢?看來只有修改代碼了啊,由於上面代碼中Jim類依賴於Apple類,所以導致不得不去改動Jim類里面的代碼。那如果下次Jim又要吃別的水果了呢?繼續修改代碼?這種處理方式顯然是不可取的,頻繁修改會帶來很大的系統風險,改着改着可能就發現Jim不會吃水果了。

  上面的代碼之所以會出現上述難堪的問題,就是因為Jim類依賴於Apple類,兩者是緊耦合的關系,其導致的結果就是系統的可維護性大大降低。要增加香蕉類卻要去修改Jim類代碼,這是不可忍受的,你改你的代碼為什么要動我的啊,顯然Jim不樂意了。我們常說要設計一個健壯穩定的系統,而這里只是增加了一個香蕉類,就要去修改Jim類,健壯和穩定還從何談起。

  而根據依賴倒置原則,我們可以對上述代碼做些修改,提取抽象的部分。首先我們提取出兩個接口:People和Fruit,都提供各自必需的抽象方法,這樣以后無論是增加Jim人類,還是增加Apple、Banana等各種水果,都只需要增加自己的實現類就可以了。由於遵循依賴倒置原則,只依賴於抽象,而不依賴於細節,所以增加類無需修改其他類。

  代碼如下:

//人接口
public interface People {
	public void eat(Fruit fruit);//人都有吃的方法,不然都餓死了
}
//水果接口
public interface Fruit {
	public String getName();//水果都是有名字的
}
//具體Jim人類
public class Jim implements People{
	public void eat(Fruit fruit){
		System.out.println("Jim eat " + fruit.getName());
	}
}
//具體蘋果類
public class Apple implements Fruit{
	public String getName(){
		return "apple";
	}
}
//具體香蕉類
public class Banana implements Fruit{
	public String getName(){
		return "banana";
	}
}
public class Client {
	public static void main(String[] args) {
		People jim = new Jim();
		Fruit apple = new Apple();
		Fruit Banana = new Banana();//這里符合了里氏替換原則
		jim.eat(apple);
		jim.eat(Banana);
	}
}

  運行結果:

  Jim eat apple
  Jim eat banana

  • People類是復雜的業務邏輯,屬於高層模塊,而Fruit是原子模塊,屬於低層模塊。People依賴於抽象的Fruit接口,這就做到了:高層模塊不應該依賴低層模塊,兩者都應該依賴於抽象(抽象類或接口)。
  • People和Fruit接口與各自的實現類沒有關系,增加實現類不會影響接口,這就做到了:抽象(抽象類或接口)不應該依賴於細節(具體實現類)。
  • Jim、Apple、Banana實現類都要去實現各自的接口所定義的抽象方法,所以是依賴於接口的。這就做到了:細節(具體實現類)應該依賴抽象。

3、什么是倒置

  到了這里,我們對依賴倒置原則的“依賴”就很好理解了,但是什么是“倒置”呢。是這樣子的,剛開始按照正常人的一般思維方式,我想吃香蕉就是吃香蕉,想吃蘋果就吃蘋果,編程也是這樣,都是按照面向實現的思維方式來設計。而現在要倒置思維,提取公共的抽象,面向接口(抽象類)編程。不再依賴於具體實現了,而是依賴於接口或抽象類,這就是依賴的思維方式“倒置”了。

4、依賴的三種實現方式

  對象的依賴關系有三種方式來傳遞:

  • 接口方法中聲明依賴對象。就是我們上面代碼所展示的那樣。
  • 構造方法傳遞依賴對象。在構造函數中的需要傳遞的參數是抽象類或接口的方式實現。代碼如下:
//具體Jim人類
public class Jim implements People{
	
	private Fruit fruit;
	
	public Jim(Fruit fruit){//構造方法傳遞依賴對象
		this.fruit = fruit;
	}
	
	public void eat(Fruit fruit){
		System.out.println("Jim eat " + this.fruit.getName());
	}
}
  • Setter方法傳遞依賴對象。在我們設置的setXXX方法中的參數為抽象類或接口,來實現傳遞依賴對象。代碼如下:
//具體Jim人類
public class Jim implements People{
	
	private Fruit fruit;
	
	public void setFruit(Fruit fruit){//setter方式傳遞依賴對象
		this.fruit = fruit;
	}
	
	public void eat(){
		System.out.println("Jim eat " + this.fruit.getName());
	}
}

5、優點

  從上面的代碼修改過程中,我們可以看到由於類之間松耦合的設計,面向接口編程依賴抽象而不依賴細節,所以在修改某個類的代碼時,不會牽涉到其他類的修改,顯著降低系統風險,提高系統健壯性。

  還有一個優點是,在我們實際項目開發中,都是多人團隊協作,每人負責某一模塊。比如一個人負責開發People模塊,一人負責開發Fruit模塊,如果未采用依賴倒置原則,沒有提取抽象,那么開發People模塊的人必須等Fruit模塊開發完成后自己才能開發,否則編譯都無法通過,這就是單線程的開發。為了能夠兩人並行開發,設計時遵循依賴倒置原則,提取抽象,就可以大大提高開發進度。

6、總結

  說到底,依賴倒置原則的核心就是面向接口編程的思想,盡量對每個實現類都提取抽象和公共接口形成接口或抽象類,依賴於抽象而不要依賴於具體實現。依賴倒置原則的本質其實就是通過抽象(抽象類或接口)使各個類或模塊的實現彼此獨立,不相互影響,實現模塊間的松耦合。但是這個原則也是6個設計原則中最難以實現的了,如果沒有實現這個原則,那么也就意味着開閉原則(對擴展開放,對修改關閉)也無法實現。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM