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