小話設計模式原則之:依賴倒置原則DIP

前言:很久之前就想動筆總結下關於軟體設計的一些原則,或者說是設計模式的一些原則,奈何被各種bootstrap組件所吸引,一直抽不開身。關於設計模式,作為程序猿的我們肯定都不陌生。博主的理解,所謂設計模式就是前人總結下來的一些對於某些特定使用場景非常適用的優秀的設計思路,「前人栽樹,後人乘涼」,作為後來者的我們就有福了,當我們遇到類似的應用場景的時候就可以直接使用了。關於設計模式的原則,博主將會在接下來的幾篇裡面根據自己的理解一一介紹,此篇就先來看看設計模式的設計原則之——依賴倒置原則。

軟體設計原則系列文章索引

  • 小話設計模式原則之:依賴倒置原則DIP

  • 小話設計模式原則之:單一職責原則SRP

  • 小話設計模式原則之:介面隔離原則ISP

  • 小話設計模式原則之:開閉原則OCP

  • 小話設計模式原則之:里氏替換原則LSP

一、原理介紹

1、官方定義

依賴倒置原則,英文縮寫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。

官方翻譯:高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。

2、自己理解

2.1、原理解釋

上面的定義不難理解,主要包含兩次意思:

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

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

2.2、被「倒置」的依賴

相比傳統的軟體設計架構,比如我們常說的經典的三層架構,UI層依賴於BLL層,BLL層依賴於DAL層。由於每一層都是依賴於下層的實現,這樣當某一層的結構發生變化時,它的上層就不得不也要發生改變,比如我們DAL裡面邏輯發生了變化,可能會導致BLL和UI層都隨之發生變化,這種架構是非常荒謬的!好,這個時候如果我們換一種設計思路,高層模塊不直接依賴低層的實現,而是依賴於低層模塊的抽象,具體表現為我們增加一個IBLL層,裡面定義業務邏輯的介面,UI層依賴於IBLL層,BLL層實現IBLL裡面的介面,所以具體的業務邏輯則定義在BLL裡面,這個時候如果我們BLL裡面的邏輯發生變化,只要介面的行為不變,上層UI裡面就不用發生任何變化。

在經典的三層裡面,高層模塊直接依賴低層模塊的實現,當我們將高層模塊依賴於底層模塊的抽象時,就好像依賴「倒置」了。這就是依賴倒置的由來。通過依賴倒置,可以使得架構更加穩定、更加靈活、更好應對需求變化。

2.3、依賴倒置的目的

上面說了,在三層架構裡面增加一個介面層能實現依賴倒置,它的目的就是降低層與層之間的耦合,使得設計更加靈活。從這點上來說,依賴倒置原則也是「松耦合」設計的很好體現。

二、場景示例

文章最開始的時候說了,依賴倒置是設計模式的設計原則之一,那麼在我們那麼多的設計模式中,哪些設計模式遵循了依賴倒置的原則呢?這個就多了,比如我們常見的工廠方法模式。下面博主就結合一個使用場景來說說依賴倒置原則如何能夠使得設計更加靈活。

場景描述:還記得在一場風花雪月的邂逅:介面和抽象類這篇裡面介紹過設備的採集的例子,這篇繼續以這個使用場景來說明。設備有很多類型,每種設備都有登錄和採集兩個方法,通過DeviceService這個服務去啟動設備的採集,最開始我們只有MML和TL2這兩種類型的設備,那麼來看看我們的設計代碼。

代碼示例:

  //MML類型的設備n public class DeviceMMLn {n public void Login()n {n Console.WriteLine("MML設備登錄");n }nn public bool Spider()n {n Console.WriteLine("MML設備採集");n return true;n }n }nn //TL2類型設備n public class DeviceTL2n {n public void Login()n {n Console.WriteLine("TL2設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TL2設備採集");n return true;n }n }nn //設備採集的服務n public class DeviceServicen {n private DeviceMML MML = null;n private DeviceTL2 TL2 = null;n private string m_type = null;n //構造函數裡面通過類型來判斷是哪種類型的設備n public DeviceService(string type)n {n m_type = type;n if (type == "0")n {n MML = new DeviceMML();n }n else if (type == "1")n {n TL2 = new DeviceTL2();n }n }nn public void LoginDevice()n {n if (m_type == "0")n {n MML.Login();n }n else if (m_type == "1")n {n TL2.Login();n }n }nn public bool DeviceSpider()n {n if (m_type == "0")n {n return MML.Spider();n }n else if (m_type == "1")n {n return TL2.Spider();n }n elsen {n return true;n }n }n }n

在Main函數裡面調用

  class Programn {nn static void Main(string[] args)n {n var oSpider = new DeviceService("1");n oSpider.LoginDevice();n var bRes = oSpider.DeviceSpider();n n Console.ReadKey();n }n

上述代碼經過開發、調試、部署、上線。可以正常運行,貌似一切都OK。

日復一日、年復一年。後來公司又來兩種新的設備TELNET和TL5類型設備。於是程序猿們又有得忙了,加班,趕進度!於是代碼變成了這樣:

   //MML類型的設備n public class DeviceMMLn {n public void Login()n {n Console.WriteLine("MML設備登錄");n }nn public bool Spider()n {n Console.WriteLine("MML設備採集");n return true;n }n }nn //TL2類型設備n public class DeviceTL2n {n public void Login()n {n Console.WriteLine("TL2設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TL2設備採集");n return true;n }n }nn //TELNET類型設備n public class DeviceTELNETn {n public void Login()n {n Console.WriteLine("TELNET設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TELNET設備採集");n return true;n }n }nn //TL5類型設備n public class DeviceTL5n {n public void Login()n {n Console.WriteLine("TL5設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TL5設備採集");n return true;n }n }nnn //設備採集的服務n public class DeviceServicen {n private DeviceMML MML = null;n private DeviceTL2 TL2 = null;n private DeviceTELNET TELNET = null;n private DeviceTL5 TL5 = null;n private string m_type = null;n //構造函數裡面通過類型來判斷是哪種類型的設備n public DeviceService(string type)n {n m_type = type;n if (type == "0")n {n MML = new DeviceMML();n }n else if (type == "1")n {n TL2 = new DeviceTL2();n }n else if (type == "2")n {n TELNET = new DeviceTELNET();n }n else if (type == "3")n {n TL5 = new DeviceTL5();n }n }nn public void LoginDevice()n {n if (m_type == "0")n {n MML.Login();n }n else if (m_type == "1")n {n TL2.Login();n }n else if (m_type == "2")n {n TELNET.Login();n }n else if (m_type == "3")n {n TL5.Login();n }n }nn public bool DeviceSpider()n {n if (m_type == "0")n {n return MML.Spider();n }n else if (m_type == "1")n {n return TL2.Spider();n }n else if (m_type == "2")n {n return TELNET.Spider();n }n else if (m_type == "3")n {n return TL5.Spider();n }n elsen {n return true;n }n }n }n

比如我們想啟動TL5類型設備的採集,這樣調用可以實現:

static void Main(string[] args)n {n var oSpider = new DeviceService("3");n oSpider.LoginDevice();n var bRes = oSpider.DeviceSpider();n n Console.ReadKey();n }n

花了九年二虎之力,總算是可以實現了。可是又過了段時間,又有新的設備類型呢?是不是又要加班,又要改。這樣下去,感覺這就是一個無底洞,再加上時間越久,項目所經歷的開發人員越容易發生變化,這個時候再改,那維護的成本堪比開發一個新的項目。並且,隨著設備類型的增多,代碼裡面充斥著大量的if...else,這樣的爛代碼簡直讓人無法直視。

基於這種情況,如果我們當初設計這個系統的時候考慮了依賴倒置,那麼效果可能截然不同。我們來看看依賴倒置如何解決以上問題的呢?

//定義一個統一介面用於依賴n public interface IDevicen {n void Login();n bool Spider();n }nn //MML類型的設備n public class DeviceMML : IDevicen {n public void Login()n {n Console.WriteLine("MML設備登錄");n }nn public bool Spider()n {n Console.WriteLine("MML設備採集");n return true;n }n }nn //TL2類型設備n public class DeviceTL2 : IDevicen {n public void Login()n {n Console.WriteLine("TL2設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TL2設備採集");n return true;n }n }nn //TELNET類型設備n public class DeviceTELNET : IDevicen {n public void Login()n {n Console.WriteLine("TELNET設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TELNET設備採集");n return true;n }n }nn //TL5類型設備n public class DeviceTL5 : IDevicen {n public void Login()n {n Console.WriteLine("TL5設備登錄");n }nn public bool Spider()n {n Console.WriteLine("TL5設備採集");n return true;n }n }nnn //設備採集的服務n public class DeviceServicen {n private IDevice m_device;n public DeviceService(IDevice oDevice)n {n m_device = oDevice;n }nn public void LoginDevice()n {n m_device.Login();n }nn public bool DeviceSpider()n {n return m_device.Spider();n }n }n

調用

     static void Main(string[] args)n {n var oSpider = new DeviceService(new DeviceTL5());n oSpider.Login();n var bRes = oSpider.Spider();nn Console.ReadKey();n }n

代碼說明:上述解決方案中,我們定義了一個IDevice介面,用於上層服務的依賴,也就是說,上層服務(這裡指DeviceService)僅僅依賴IDevice介面,對於具體的實現類我們是不管的,只要介面的行為不發生變化,增加新的設備類型後,上層服務不用做任何的修改。這樣設計降低了層與層之間的耦合,能很好地適應需求的變化,大大提高了代碼的可維護性。呵呵,看著是不是有點眼熟?是不是有點像某個設計模式?其實設計模式的設計原理正是基於此。

三、使用Unity實現依賴倒置

上面說了那麼多,都是在講依賴倒置的好處,那麼在我們的項目中究竟如何具體實現和使用呢?

在介紹依賴倒置具體如何使用之前,我們需要引入IOC容器相關的概念,我們先來看看它們之間的關係。

依賴倒置原則(DIP):一種軟體架構設計的原則(抽象概念)。

控制反轉(IoC):一種反轉流、依賴和介面的方式(DIP的具體實現方式)。這是一個有點不太好理解和解釋的概念,通俗地說,就是應用程序本身不負責依賴對象的創建和維護,而是將它交給一個外部容器(比如Unity)來負責,這樣控制權就由應用程序轉移到了外部IoC 容器,即控制權實現了所謂的反轉。例如在類型A中需要使用類型B的實例,而B 實例的創建並不由A 來負責,而是通過外部容器來創建。

依賴注入(DI):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)。也有很多博文裡面說IOC也叫DI,其實根據博主的理解,DI應該是IOC的具體實現方式,比如我們如何實現控制反轉,答案就是通過依賴注入去實現。

IoC容器:依賴注入的框架,用來映射依賴,管理對象創建和生存周期(DI框架),自動創建、維護依賴對象。

關於Ioc容器,各個語言都有自己的成熟的解決方案,比如Java裡面最偉大的框架之一Spring,.net裡面輕量級的Autofac等。由於博主對C#語言相對來說比較熟悉,這裡就結合C#裡面的一款ioc容器來舉例說明。.net裡面常用的ioc容器:

  • Spring.NETspringframework.net/
  • Unityunity.codeplex.com/
  • Autofac: code.google.com/p/autof
  • Ninject: ninject.org/

當然,還有其他的IOC容器這裡就不一一列舉。下面博主還是就Unity這種IOC容器來看看依賴倒置的具體實現。

1、Unity引入

Unity如何引入?我們神奇的Nuget又派上用場了。最新的Unity版本已經到了4.0.1。

安裝成功後主要引入了三個dll。

2、Unity常用API

UnityContainer.RegisterType<ITFrom,TTO>();nnUnityContainer.RegisterType< ITFrom, TTO >();nnUnityContainer.RegisterType< ITFrom, TTO >("keyName");nnIEnumerable<T> databases = UnityContainer.ResolveAll<T>();nnIT instance = UnityContainer.Resolve<IT>();nnT instance = UnityContainer.Resolve<T>("keyName");nnUnitContainer.RegisterInstance<T>("keyName",new T());nnUnityContainer.BuildUp(existingInstance);nnIUnityContainer childContainer1 = parentContainer.CreateChildContainer();n

3、代碼注入方式示例

3.1、默認註冊方式

仍然以上面的場景為例說明,我們注入DeviceMML這個實現類。

class Programn {n private static IUnityContainer container = null;n static void Main(string[] args)n {n RegisterContainer();n var oSpider = container.Resolve<IDevice>();n oSpider.Login();n var bRes = oSpider.Spider();nn Console.ReadKey();n }nn /// <summary>n /// 代碼注入n /// </summary>n public static void RegisterContainer()n {n container = new UnityContainer();n container.RegisterType<IDevice, DeviceMML>(); //默認註冊方式,如果後面再次默認註冊會覆蓋前面的n }n }n

運行結果

3.2、帶命名方式的註冊

上面默認注入的方式中,我們只能注入一種具體的實例,如果我們需要同時注入多個類型的實例呢?看看我們的 RegisterType() 方法有多個重載。

   class Programn {n private static IUnityContainer container = null;n static void Main(string[] args)n {n RegisterContainer();n var oSpider = container.Resolve<IDevice>("TL5");n oSpider.Login();n var bRes = oSpider.Spider();nn Console.ReadKey();n }nn /// <summary>n /// 代碼注入n /// </summary>n public static void RegisterContainer()n {n container = new UnityContainer();n container.RegisterType<IDevice, DeviceMML>("MML"); //默認註冊(無命名),如果後面還有默認註冊會覆蓋前面的n container.RegisterType<IDevice, DeviceTELNET>("Telnet"); //命名註冊n container.RegisterType<IDevice, DeviceTL2>("TL2"); //命名註冊n container.RegisterType<IDevice, DeviceTL5>("TL5"); //命名註冊n }n }n

運行結果

4、配置文件注入方式示例

在App.config或者Web.config裡面加入如下配置:

<?xml version="1.0" encoding="utf-8" ?>n<configuration>n <configSections>n <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>n </configSections>n <unity>n <!--容器-->n <containers>n <container name="Spider">n <!--映射關係-->n <register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>n <register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTELNET,ESTM.Spider" name="TELNET"></register>n <register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTL2,ESTM.Spider" name="TL2"></register>n <register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTL5,ESTM.Spider" name="TL5"></register>n </container>n </containers>n </unity>n</configuration>n

在代碼裡面註冊配置文件:

namespace ESTM.Spidern{n class Programn {n private static IUnityContainer container = null;n static void Main(string[] args)n {n ContainerConfiguration();n var oSpider = container.Resolve<IDevice>("TL5");n oSpider.Login();n var bRes = oSpider.Spider();nn Console.ReadKey();n }nn /// <summary>n /// 配置文件注入n /// </summary>n public static void ContainerConfiguration()n {n container = new UnityContainer();n UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);n configuration.Configure(container, "Spider");n }nn }n}n

運行結果:

代碼說明

(1)

<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>n

節點裡面,type對象抽象,mapTo對象具體實例對象,name對象實例的別名。

(2)在app.config裡面可以配置多個 <container name="Spider"> 節點,不同的name配置不同的依賴對象。

(3)配置文件注入的靈活之處在於解耦。為什麼這麼說呢?試想,如果我們的IDevice介面對應著一個介面層,而DeviceMML、DeviceTELNET、DeviceTL2、DeviceTL5等實現類在另外一個實現層裡面,我們的UI層(這裡對應控制台程序這一層)只需要添加IDevice介面層的引用,不必添加實現層的引用,通過配置文件注入,在運行的時候動態將實現類注入到UI層裡面來。這樣UI層就對實現層實現了解耦,實現層裡面的具體邏輯變化時,UI層裡面不必做任何更改。

四、總結

到此,依賴倒置原則的講解基本結束了。根據博主的理解,設計模式的這些原則是設計模式的理論指導,而設計模式則是這些理論的具體運用。說一千道一萬,要想搞懂設計模式,必須先了解設計模式遵循的原則,無論是哪種設計模式都會遵循一種或者多種原則。當然文章可能有理解不當的地方,歡迎大牛們指出。

當然如果你覺得本文對你有幫助,可以打賞博主,也可以點贊以資鼓勵。不管是物資獎勵還是精神支持,都是對博主分享精神的肯定,博主一定繼續努力。


推薦閱讀:

讓代碼變立體
再談什麼是軟體架構
停下來,歇口氣,造輪子
從微服務聊起

TAG:设计原则 | 设计模式 | 软件架构 |