DIP原则、IoC以及DI
一、DIP原則
高層模塊不應該依賴于底層模塊,二者都應該依賴于抽象。
抽象不應該依賴于細節,細節應該依賴于抽象。
該原則理解起來稍微有點抽象,我們可以將該原則通俗的理解為:"依賴于抽象”。
該規則告訴我們,程序中所有的依賴關系都應該終止于抽象類或者接口,從而達到松耦合的目的。因為我們在應用程序中編寫的大多數具體類都是不穩定的。我們不想直接依賴于這些不穩定的具體類。通過把它們隱藏在抽象和接口的后面,可以隔離它們的不穩定性。
舉個例子
一個Button對象會觸發Click方法,當被按下時,會調用Light對象的TurnOn方法,否則會調用Light對象的TurnOff方法。
這個設計存在兩個問題:
Button類直接依賴于Light類,這種依賴關系意味著當Light改變時,Button類會受到影響;
Button對象只能控制Light對象,想要控制電視或者冰箱就不行了;
新的設計:
這個方案對那些需要被Button控制的對象提出了一個約束。需要被Button控制的對象必須要實現ISwitchableDevice接口。
所為原則,只是描述了什么是對的,但是并沒有說清楚如何去做。在軟件工程中,我們經常使用DI(依賴注入)來達到這個目的。但是提到依賴注入,人們又會經常提起IoC這個術語。所以先讓我們來了解下什么是IoC。
二、IoC
IoC的全名是Inverse of Control,即控制反轉。這一術語并不是用來描述面向對象的某種原則或者模式,IoC體現為一種流程控制的反轉,一般用來對框架進行設計。
舉個例子
ReportService是一個用來顯示報表的流程,該流程包括Trim(),Clean(),Show()三個環節。
public class ReportService{? ?private string _data; ?
?public ReportService(string data) ? ?{_data = data;} ? ?public void Trim(string data) ? ?{_data = data.Trim();} ? ?public void Clean() ? ?{_data = _data.Replace("@", "");_data = _data.Replace("-", ""); ? ? ? ?//...other rules} ? ?public void Show() ? ?{Console.WriteLine(_data);}}
客戶端通過下面的方式使用該服務:
var reportService = new ReportService(input); reportService.Trim(input); reportService.Clean(); reportService.Show();這樣的一個設計體現了過程式的思考方式,客戶端依次調用每個環節從而組成了整個報表顯示流程,這樣的代碼體現了:客戶端擁有流程控制權。
我們來分析下這段代碼,ReportService提供了3個可重用的Api,正如ReportService的命名一樣,它告訴我們它是一個服務,我們只能重用他提供的三個服務,它無法提供一個打印報表的流程,整個流程是客戶端來控制的。
另外,該設計也違反了tell, Don't ask原則。
打印報表作為一個可復用的流程,不但可以提供可復用的流程環節,還可以提供可復用的流程的定義,當我們進行框架設計的時候,往往會將整個流程控制定制在框架之中,然后提供擴展點供客戶端定制。這樣的思想體現了流程的所有權從客戶端到框架的反轉。
比如asp.net mvc或者asp.net api框架,內部定義了http消息從請求,model binder,controller的激活,action的執行,返回response
等可復用的流程。同時還提供了每一個環節的可擴展點。
利用以上思想,我們對ReportService重新設計。
新的設計
采用IoC思想重新設計該報表服務,將原來客戶端擁有的流程控制權反轉在報表服務框架中。ReportService這樣的命名已經不適合我們的想法,新的實現不但提供了報表打印的相關服務,同時還提供了一個可復用的流程,因此重新命名為ReportEngine。我們可以通過模板方法達到此目的:
public class ReportEngine{ ? ?private ?string _data; ? ?public ReportEngine(string data) ? ?{_data = data;} ? ?public void Show() ? ?{Trim();Clean();Display();} ? ?public virtual void Trim() ? ?{_data = _data.Trim();} ? ?public virtual void Clean() ? ?{_data = _data.Replace("@", "");_data = _data.Replace("-", "");} ? ?public virtual void Display() ? ?{Console.WriteLine(_data);}}此時的報表服務在Show()方法中定義好了一組可復用的流程,客戶端只需要根據自己的需求重寫每個環節即可。客戶端可以通過下面的方式使用ReportEngine
var reportEngine=new StringReportEngine(input); reportEngine.Show();三、DI(Dependency Injection)
DI即依賴注入,主要解決了2個問題:
松耦合,由DI容器來創建對象,符合DIP原則;
符合IoC的思想,整個應用程序事先定義好了一套可工作的流程,通過在客戶端替換DI容器中的具體實現達到重寫某個組件的目的;
除此之外,使用依賴注入還可以帶來以下好處:
促使你寫出更加符合面向對象原則的代碼,符合優先使用對象組合,而不是繼承的原則;
使系統更加具有可測試性;
使系統更加具備可擴展性和可維護性;
由于所有組件都由DI容器管理,所以可以很方便的實現AOP攔截
我記得之前在stackoverflow上看到過類似這樣的一個問題:
如何給5歲小孩解釋什么叫DI?
得分最高的答案是:小孩在餓的時候只需喊一聲我要吃飯即可,而無需關注吃什么,飯是怎么來的等問題。
public class Kid{ ? ?private readonly IFoodSupplier _foodSupplier; ? ?public Kid(IFoodSupplier foodSupplier) ? ?{_foodSupplier = foodSupplier;} ? ?public void HaveAMeal() ? ?{ ? ? ? ?var food = _foodSupplier.GetFood(); ? ? ? ?//eat} }DI的背后是一個DI Container(DI容器)在發揮作用。DI之所以能夠工作需要兩個步驟:
將組件注冊到DI容器中;
DI容器統一管理所有依賴關系,將依賴組件注入到所需要相應的組件中;
3.1 組件的注冊方式
組件注冊到DI容器中有3種方式:
通過XML文件注冊
通過Attribute(Annotation)注冊
通過DI容器提供的API注冊
.net平臺中的大多數DI框架都通過第三種方式進行組件注冊,為了介紹這3種不同的注冊方式,我們通過Java平臺下的Spring框架簡單介紹:Java中的Spring最早以XML文件的方式進行組件注冊,發展到目前主要通過Annotation來注冊。
假如我們有CustomerRepository接口和相應的實現CustomerRepositoryImpl,下面用三種不同的方式將CustomerRepository和CustomerRepositoryImpl的對應關系注冊在DI容器中:
?List<Customer> findAll(); }
?
?public class CustomerRepositoryImpl implements CustomerRepository { ? ?public List<Customer> findAll() { ? ?
? ? ?List<Customer> customers = new ArrayList<Customer>();Customer customer = new Customer("Bryan","Hansen");customers.add(customer); ? ?
? ? ?? ?return customers;} }
3.1.1、xml文件注冊
<bean name="customerRepository" class="com.thoughtworks.xml.repository.CustomerRepositoryImpl"/>3.1.2、Annotation注冊
("customerRepository")public class CustomerRepositoryImpl implements CustomerRepository { ?
?public List<Customer> findAll() { ? ? ? //...} }
3.1.3、通過Java代碼來實現注冊
public class AppConfig { ??(name = "customerRepository") ?
?public CustomerRepository getCustomerRepository() { ? ?
? ?return new CustomerRepositoryImpl();} }
3.1.4通過下面的方式從Container來獲取一個實例
appContext.getBean("customerService", CustomerService.class);一旦我們將所有組件都注冊在容器中,就可以靠DI容器進行依賴注入了。
3.2 依賴注入的三種方式
3.2.1. 構造器注入
正如上面Kid的實現一樣,我們通過構造器來注入IFoodSupplier組件,這種方式也是依賴注入最佳方式。
3.2.2. 屬性注入
public class Kid2{ ??public IFoodSupplier FoodSupplier { get; set; } ?
?
??public void HaveAMeal() ? ?{ ? ? ?
??
???var food = FoodSupplier.GetFood(); ? ? ? ?//eat} }
即通過一個可讀寫的屬性完成注入,該方案的缺點在于為了達到依賴注入的目的而破壞了對象的封裝性,所以不推薦。
3.2.3 方法注入
通過添加方法的參數來完成注入,一般來說這種方式都可以通過構造器注入的方式來替換,所以也不太常用。值得一提的是asp.net core源碼中用到了這種注入方式。
四、依賴注入實例
1、Register Resolve Release Pattern
下面描述了一個很簡單的Console application, 所有的組件都通過Castle Windsor容器進行構造器注入:
//registervar ?container = new WindsorContainer(); container.Register(Component.For<IParser>().ImplementedBy<Parser>()); container.Register(Component.For<IWriter>().ImplementedBy<Writer>()); container.Register(Component.For<Application>());//resolvevar application = container.Resolve<Application>(); application.Execute("hel--lo, wor--ld");//releasecontainer.Release(application);這個例子向我們展示了一個最簡單的依賴注入使用方式,register所有組件,resolve客戶端程序,最后的release步驟向我們展示了如果顯示從DI容器得到一個對象,應該顯示釋放該組件。這一步在大多數情況下并不是必須的,但是在特定場景下會發生內存泄漏。
2、.net平臺下依賴注入最佳實踐
下面的解決方案描述了一個典型的應用程序分層結構,該分層結構用來描述如何使用Catle windsor進行依賴注入,注意:這并不是一個合理的領域驅動案例,例如我將Domain模型引用到了Application或者ApplicationService程序集中。
處在項目最底層的Repository程序集定義了一組UserRepository及其接口IUserRepository,這樣的一個組件如何注冊在Windsor Container中呢?Castle提供了一種叫做WindsorInstaller的機制:
該Installer利用Fluent Api定義了IUserRepository和UserRepository的對應關系,相對于Java中的Spring框架提供的代碼注冊方式,該方案的優越之處是顯而易見的。
另外的重點在于該Installer此時并沒有執行,只有當客戶端調用此Installer時,該組件才真真注冊進容器。這一點很關鍵,我們后面還會提到。
接下來的ApplicationService層使用了Repository的抽象,一個典型的使用片斷如下:
public class UserApplicationService : IUserApplicationService{ ? ?? private readonly IUserRepository _userRepository;
? ?public UserApplicationService(IUserRepository userRepository) ?
??{_userRepository = userRepository;} ?
? ?
? ??public void Register(User user) ? ?{_userRepository.Save(user);} ? ?//.....}
我們通過構造器注入的方式注入了IUserRepository,同時,作為Service層,它也擁有自己的Installer:
public class ApplicationServiceInstaller:IWindsorInstaller{? ? ? ? ?public void Install(IWindsorContainer container, IConfigurationStore store) ? ?{container.Register(Classes.FromThisAssembly().BasedOn<IApplicationService>().WithServiceDefaultInterfaces().LifestyleScoped());} }
上面的例子示范了如何通過Castle提供的高級api來實現將該程序集中所有繼承于IApplicationService的組件和其默認接口一次性全部注冊到DI容器中。
比如UserApplicationService和IUserApplicationService,以及未來將要實現的OrderApplicationService以及IOrderApplicationService。
接下來到客戶端程序集Application層,Application作為使用ApplicationService程序集的客戶端,他才擁有將組件注冊進DI容器的能力,我們定義一個ApplicationBootstrap來初始化DI容器并注冊組件:
public class ApplicationBootstrap{ ?? ?public static IWindsorContainer Container { get; private set; } ?
?
? ?public static IWindsorContainer ?RegisterComponents() ? ?{Container=new WindsorContainer();Container.Install(FromAssembly.This());Container.Install(FromAssembly.Containing<ApplicationServiceInstaller>());Container.Install(FromAssembly.Containing<RepositoryInstaller>()); ? ? ? ?return Container;} }
注意Container.Install(...)方法將執行不同應用程序的Installer,此時組件才真真注冊進DI容器。該實例展示了如何正確的使用依賴注入框架:
不同的程序集之間通過接口依賴,符合DIP原則;
通過依賴注入的方式定義好了可運行的流程,但是客戶端可以注冊不同的組件到DI容器中,符合IoC的思想;
3、如何在asp.net mvc和asp.net webapi使用依賴注入
本文提供的源碼中所含的WebApplicationSample項目演示了如何通過自定義WindsorControllerFactory來實現mvc的依賴注入,通過自定義WindsorCompositionRoot實現web api的依賴注入。
五、高級進階
asp.net core實現了一個還算簡單的DI容器DenpendencyInjection,感興趣的同學可以閱讀其源碼。
原文地址:http://www.cnblogs.com/richieyang/p/6060160.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的DIP原则、IoC以及DI的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用JWT来保护我们的ASP.NET Co
- 下一篇: Connect 2016 白话脱口秀将在