小話設計模式原則之:依賴倒置原則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類型的設備
public class DeviceMML
{
public void Login()
{
Console.WriteLine("MML設備登錄");
}
public bool Spider()
{
Console.WriteLine("MML設備采集");
return true;
}
}
//TL2類型設備
public class DeviceTL2
{
public void Login()
{
Console.WriteLine("TL2設備登錄");
}
public bool Spider()
{
Console.WriteLine("TL2設備采集");
return true;
}
}
//設備采集的服務
public class DeviceService
{
private DeviceMML MML = null;
private DeviceTL2 TL2 = null;
private string m_type = null;
//構造函數里面通過類型來判斷是哪種類型的設備
public DeviceService(string type)
{
m_type = type;
if (type == "0")
{
MML = new DeviceMML();
}
else if (type == "1")
{
TL2 = new DeviceTL2();
}
}
public void LoginDevice()
{
if (m_type == "0")
{
MML.Login();
}
else if (m_type == "1")
{
TL2.Login();
}
}
public bool DeviceSpider()
{
if (m_type == "0")
{
return MML.Spider();
}
else if (m_type == "1")
{
return TL2.Spider();
}
else
{
return true;
}
}
}
在Main函數里面調用
class Program
{
static void Main(string[] args)
{
var oSpider = new DeviceService("1");
oSpider.LoginDevice();
var bRes = oSpider.DeviceSpider();
Console.ReadKey();
}
上述代碼經過開發、調試、部署、上線。可以正常運行,貌似一切都OK。
日復一日、年復一年。后來公司又來兩種新的設備TELNET和TL5類型設備。于是程序猿們又有得忙了,加班,趕進度!于是代碼變成了這樣:
//MML類型的設備
public class DeviceMML
{
public void Login()
{
Console.WriteLine("MML設備登錄");
}
public bool Spider()
{
Console.WriteLine("MML設備采集");
return true;
}
}
//TL2類型設備
public class DeviceTL2
{
public void Login()
{
Console.WriteLine("TL2設備登錄");
}
public bool Spider()
{
Console.WriteLine("TL2設備采集");
return true;
}
}
//TELNET類型設備
public class DeviceTELNET
{
public void Login()
{
Console.WriteLine("TELNET設備登錄");
}
public bool Spider()
{
Console.WriteLine("TELNET設備采集");
return true;
}
}
//TL5類型設備
public class DeviceTL5
{
public void Login()
{
Console.WriteLine("TL5設備登錄");
}
public bool Spider()
{
Console.WriteLine("TL5設備采集");
return true;
}
}
//設備采集的服務
public class DeviceService
{
private DeviceMML MML = null;
private DeviceTL2 TL2 = null;
private DeviceTELNET TELNET = null;
private DeviceTL5 TL5 = null;
private string m_type = null;
//構造函數里面通過類型來判斷是哪種類型的設備
public DeviceService(string type)
{
m_type = type;
if (type == "0")
{
MML = new DeviceMML();
}
else if (type == "1")
{
TL2 = new DeviceTL2();
}
else if (type == "2")
{
TELNET = new DeviceTELNET();
}
else if (type == "3")
{
TL5 = new DeviceTL5();
}
}
public void LoginDevice()
{
if (m_type == "0")
{
MML.Login();
}
else if (m_type == "1")
{
TL2.Login();
}
else if (m_type == "2")
{
TELNET.Login();
}
else if (m_type == "3")
{
TL5.Login();
}
}
public bool DeviceSpider()
{
if (m_type == "0")
{
return MML.Spider();
}
else if (m_type == "1")
{
return TL2.Spider();
}
else if (m_type == "2")
{
return TELNET.Spider();
}
else if (m_type == "3")
{
return TL5.Spider();
}
else
{
return true;
}
}
}
比如我們想啟動TL5類型設備的采集,這樣調用可以實現:
static void Main(string[] args)
{
var oSpider = new DeviceService("3");
oSpider.LoginDevice();
var bRes = oSpider.DeviceSpider();
Console.ReadKey();
}
花了九年二虎之力,總算是可以實現了。可是又過了段時間,又有新的設備類型呢?是不是又要加班,又要改。這樣下去,感覺這就是一個無底洞,再加上時間越久,項目所經歷的開發人員越容易發生變化,這個時候再改,那維護的成本堪比開發一個新的項目。并且,隨著設備類型的增多,代碼里面充斥著大量的if...else,這樣的爛代碼簡直讓人無法直視。
基于這種情況,如果我們當初設計這個系統的時候考慮了依賴倒置,那么效果可能截然不同。我們來看看依賴倒置如何解決以上問題的呢?
//定義一個統一接口用于依賴
public interface IDevice
{
void Login();
bool Spider();
}
//MML類型的設備
public class DeviceMML : IDevice
{
public void Login()
{
Console.WriteLine("MML設備登錄");
}
public bool Spider()
{
Console.WriteLine("MML設備采集");
return true;
}
}
//TL2類型設備
public class DeviceTL2 : IDevice
{
public void Login()
{
Console.WriteLine("TL2設備登錄");
}
public bool Spider()
{
Console.WriteLine("TL2設備采集");
return true;
}
}
//TELNET類型設備
public class DeviceTELNET : IDevice
{
public void Login()
{
Console.WriteLine("TELNET設備登錄");
}
public bool Spider()
{
Console.WriteLine("TELNET設備采集");
return true;
}
}
//TL5類型設備
public class DeviceTL5 : IDevice
{
public void Login()
{
Console.WriteLine("TL5設備登錄");
}
public bool Spider()
{
Console.WriteLine("TL5設備采集");
return true;
}
}
//設備采集的服務
public class DeviceService
{
private IDevice m_device;
public DeviceService(IDevice oDevice)
{
m_device = oDevice;
}
public void LoginDevice()
{
m_device.Login();
}
public bool DeviceSpider()
{
return m_device.Spider();
}
}
調用
static void Main(string[] args)
{
var oSpider = new DeviceService(new DeviceTL5());
oSpider.Login();
var bRes = oSpider.Spider();
Console.ReadKey();
}
代碼說明: 上述解決方案中,我們定義了一個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容器:
- http:// Spring.NET : http://www. springframework.net/
- Unity : http:// unity.codeplex.com/
- Autofac : http:// code.google.com/p/autof ac/
- Ninject : http://www. ninject.org/
當然,還有其他的IOC容器這里就不一一列舉。下面博主還是就Unity這種IOC容器來看看依賴倒置的具體實現。
1、Unity引入
Unity如何引入?我們神奇的Nuget又派上用場了。最新的Unity版本已經到了4.0.1。
安裝成功后主要引入了三個dll。
2、Unity常用API
UnityContainer.RegisterType<ITFrom,TTO>();
UnityContainer.RegisterType< ITFrom, TTO >();
UnityContainer.RegisterType< ITFrom, TTO >("keyName");
IEnumerable<T> databases = UnityContainer.ResolveAll<T>();
IT instance = UnityContainer.Resolve<IT>();
T instance = UnityContainer.Resolve<T>("keyName");
UnitContainer.RegisterInstance<T>("keyName",new T());
UnityContainer.BuildUp(existingInstance);
IUnityContainer childContainer1 = parentContainer.CreateChildContainer();
3、代碼注入方式示例
3.1、默認注冊方式
仍然以上面的場景為例說明,我們注入DeviceMML這個實現類。
class Program
{
private static IUnityContainer container = null;
static void Main(string[] args)
{
RegisterContainer();
var oSpider = container.Resolve<IDevice>();
oSpider.Login();
var bRes = oSpider.Spider();
Console.ReadKey();
}
/// <summary>
/// 代碼注入
/// </summary>
public static void RegisterContainer()
{
container = new UnityContainer();
container.RegisterType<IDevice, DeviceMML>(); //默認注冊方式,如果后面再次默認注冊會覆蓋前面的
}
}
運行結果
3.2、帶命名方式的注冊
上面默認注入的方式中,我們只能注入一種具體的實例,如果我們需要同時注入多個類型的實例呢?看看我們的 RegisterType() 方法有多個重載。
class Program
{
private static IUnityContainer container = null;
static void Main(string[] args)
{
RegisterContainer();
var oSpider = container.Resolve<IDevice>("TL5");
oSpider.Login();
var bRes = oSpider.Spider();
Console.ReadKey();
}
/// <summary>
/// 代碼注入
/// </summary>
public static void RegisterContainer()
{
container = new UnityContainer();
container.RegisterType<IDevice, DeviceMML>("MML"); //默認注冊(無命名),如果后面還有默認注冊會覆蓋前面的
container.RegisterType<IDevice, DeviceTELNET>("Telnet"); //命名注冊
container.RegisterType<IDevice, DeviceTL2>("TL2"); //命名注冊
container.RegisterType<IDevice, DeviceTL5>("TL5"); //命名注冊
}
}
運行結果
4、配置文件注入方式示例
在App.config或者Web.config里面加入如下配置:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity>
<!--容器-->
<containers>
<container name="Spider">
<!--映射關系-->
<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>
<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTELNET,ESTM.Spider" name="TELNET"></register>
<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTL2,ESTM.Spider" name="TL2"></register>
<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceTL5,ESTM.Spider" name="TL5"></register>
</container>
</containers>
</unity>
</configuration>
在代碼里面注冊配置文件:
namespace ESTM.Spider
{
class Program
{
private static IUnityContainer container = null;
static void Main(string[] args)
{
ContainerConfiguration();
var oSpider = container.Resolve<IDevice>("TL5");
oSpider.Login();
var bRes = oSpider.Spider();
Console.ReadKey();
}
/// <summary>
/// 配置文件注入
/// </summary>
public static void ContainerConfiguration()
{
container = new UnityContainer();
UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);
configuration.Configure(container, "Spider");
}
}
}
運行結果:
代碼說明
(1)
<register type="ESTM.Spider.IDevice,ESTM.Spider" mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>
節點里面,type對象抽象,mapTo對象具體實例對象,name對象實例的別名。
(2)在app.config里面可以配置多個 <container name="Spider"> 節點,不同的name配置不同的依賴對象。
(3)配置文件注入的靈活之處在于解耦。為什么這么說呢?試想,如果我們的IDevice接口對應著一個接口層,而DeviceMML、DeviceTELNET、DeviceTL2、DeviceTL5等實現類在另外一個實現層里面,我們的UI層(這里對應控制臺程序這一層)只需要添加IDevice接口層的引用,不必添加實現層的引用,通過配置文件注入,在運行的時候動態將實現類注入到UI層里面來。這樣UI層就對實現層實現了解耦,實現層里面的具體邏輯變化時,UI層里面不必做任何更改。
四、總結
到此,依賴倒置原則的講解基本結束了。 根據博主的理解,設計模式的這些原則是設計模式的理論指導,而設計模式則是這些理論的具體運用 。說一千道一萬,要想搞懂設計模式,必須先了解設計模式遵循的原則,無論是哪種設計模式都會遵循一種或者多種原則。當然文章可能有理解不當的地方,歡迎大牛們指出。
來自:https://zhuanlan.zhihu.com/p/24175489