設計原則之【依賴反轉原則】

表妹讓我幫她修收音機,把我給整懵了…

表妹:哥啊,我的電腦藍屏死機了😓

我:我幫你看看。

一頓操作…

我:你這裡有兩個內存條,但是其中一根壞了,我現在把它給卸了,暫時用那根好的。

表妹:哇!果然可以了,真​棒👍對了,哥啊,我老爸有台82年的收音機,壞了好一陣子了,你也幫他修修唄?

我:哈…?這,我就不會啦。

表妹:為啥?電腦都會,小收音機怎麼可能難倒你呢?

我:哈哈,因為,它違背了依賴反轉原則。

表妹:哈?啥意思?


 

高層模塊不要依賴底層模塊。高層模塊和底層模塊應該通過抽象來互相依賴。除此之外,抽象不要依賴具體實現細節,具體實現細節依賴抽象。

如下圖所示:

 

為什麼收音機很難修呢?

因為各個部件相互依賴,如左圖所示,任何問題都可能涉及其他部件,不懂的人根本沒法修。但是電腦不同,因為無論主板、CPU、內存、硬盤都是針對接口設計的,如右圖所示,那麼,不管哪一個出問題,都可以在不影響別的部件的前提下,進行修改或替換。

比如,現在我們需要實現一個披薩店,該店售有芝士披薩、海鮮披薩、超級至尊等品種。可能我們很容易想到,披薩店是上層模塊,具體的披薩品種是下層模塊,如果我們把披薩店和披薩品種的關係畫成一張圖,應該是這樣的:

 

接下來,我們來看一下代碼實現:

public class PizzaStore {

   public void cheesePizza() {
       System.out.println("芝士披薩");
  }

   public void seafoodPizza() {
       System.out.println("海鮮披薩");
  }
   
   public void superSupreme() {
       System.out.println("超級至尊");
  }
}

我們來模擬上層調用:

public static void main(String[] args) {
   PizzaStore pizzaStore = new PizzaStore();
   pizzaStore.cheesePizza();
   pizzaStore.superSupreme();
   pizzaStore.seafoodPizza();
}

如果該店業務的擴展,新增很多品種,那麼,這個時候,就需要從底層實現到高層調用依次地修改代碼。

我們需要在PizzaStore類中新增披薩的品種,也需要在高層調用中增加調用,這樣一來,系統發佈後,其實是非常不穩定的。雖然這個例子比較簡單,修改也不會引入新的bug,但是,實際項目會複雜很多。

最理想的情況是,我們已經編寫好的代碼可以「萬年不變」,這就意味着已經覆蓋的單元測試可以不用修改,已經存在的行為可以保證保持不變,這就意味着穩定。任何代碼上的修改帶來的影響都是有未知風險的,不論看上去多麼簡單。

如何降低高低層代碼之間的耦合?

比如,商品經濟的萌芽時期,出現了物物交換。如果你要買一台手機,賣手機的人讓你拿一頭豬來換,但是你手裡沒有豬,這時,你就要去找一個賣豬的老闆,但是他要你拿羊來跟他換,你也沒有羊,繼續去找賣羊的人…

你看,這一連串的對象依賴,造成了嚴重的耦合災難。解決這一問題的最好辦法就是,買賣雙方都依賴一個抽象–貨幣,通過貨幣來進行交換,這樣一來耦合度就大為降低了。

我們現在的代碼是上層直接依賴底層實現,現在我們需要定義一個抽象的IPizza接口,來對這種強依賴進行解耦。如下圖所示:

 

我們再來看一下代碼:

先定義一個Pizza的抽象接口IPizza:

public interface IPizza {
   void pizza();
}

接下來就是不同的品種的實現類:

public class cheesePizza implements IPizza {
   @Override
   public void pizza() {
       System.out.println("芝士披薩");
  }
}

public class seafoodPizza implements IPizza {
   @Override
   public void pizza() {
       System.out.println("海鮮披薩");
  }
}

public class superSupreme implements IPizza {
   @Override
   public void pizza() {
       System.out.println("超級至尊");
  }
}

這時上層PizzaStore類依賴IPizza接口即可:

public class PizzaStore {
   public void sellPizza(IPizza p) {
       p.pizza();
  }
}

接下來就是上層調用:

public static void main(String[] args) {
   PizzaStore pizzaStore = new PizzaStore();
   pizzaStore.sellPizza(new cheesePizza());
   pizzaStore.sellPizza(new seafoodPizza());
   pizzaStore.sellPizza(new superSupreme());
}

你看,在這種設計下,無論該披薩店的品種怎麼擴增,只需新建一個該品種的實現類即可,而不需要修改底層的代碼。後面,如果該披薩店的業務繼續擴大,除了賣披薩,還賣其他小吃,飲料酒水等,同樣,只需分別抽象出小吃和飲料酒水兩個接口,讓上層調用只依賴這些接口即可。

總結

以抽象為基準比以細節為基準搭建起來的架構要穩定得多。

因此,在拿到需求後,要面向接口編程,先頂層設計再細節地設計代碼結構。

好啦,每一種設計原則是否運用得當,應該根據具體業務場景,具體分析。

參考

極客時間專欄《設計模式之美》

《大話設計模式》

//www.jianshu.com/p/c3ce6762257c

//www.wmyskxz.com/2019/11/18/tan-yi-tan-yi-lai-dao-zhi-yuan-ze/