DIP原則、IoC以及DI

一、DIP原則

  • 高層模塊不應該依賴於底層模塊,二者都應該依賴於抽象。
  • 抽象不應該依賴於細節,細節應該依賴於抽象。

該原則理解起來稍微有點抽象,我們可以將該原則通俗的理解爲:"依賴於抽象”

該規則告訴我們,程序中所有的依賴關係都應該終止於抽象類或者接口,從而達到鬆耦合的目的。因爲我們在應用程序中編寫的大多數具體類都是不穩定的。我們不想直接依賴於這些不穩定的具體類。通過把它們隱藏在抽象和接口的後面,可以隔離它們的不穩定性。

舉個例子

一個Button對象會觸發Click方法,當被按下時,會調用Light對象的TurnOn方法,否則會調用Light對象的TurnOff方法。

這個設計存在兩個問題:

  1. Button類直接依賴於Light類,這種依賴關係意味着當Light改變時,Button類會受到影響;
  2. 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個問題:

  1. 鬆耦合,由DI容器來創建對象,符合DIP原則;
  2. 符合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之所以能夠工作需要兩個步驟:

  1. 將組件註冊到DI容器中;
  2. DI容器統一管理所有依賴關係,將依賴組件注入到所需要相應的組件中;

3.1 組件的註冊方式

組件註冊到DI容器中有3種方式:

  1. 通過XML文件註冊
  2. 通過Attribute(Annotation)註冊
  3. 通過DI容器提供的API註冊

.net平臺中的大多數DI框架都通過第三種方式進行組件註冊,爲了介紹這3種不同的註冊方式,我們通過Java平臺下的Spring框架簡單介紹:Java中的Spring最早以XML文件的方式進行組件註冊,發展到目前主要通過Annotation來註冊。
假如我們有CustomerRepository接口和相應的實現CustomerRepositoryImpl,下面用三種不同的方式將CustomerRepositoryCustomerRepositoryImpl的對應關係註冊在DI容器中:

public interface CustomerRepository {
    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註冊

@Repository("customerRepository")
public class CustomerRepositoryImpl implements CustomerRepository {
    public List<Customer> findAll() {
       //...
    }
}

3.1.3、通過Java代碼來實現註冊

@Configuration
public class AppConfig {
    @Bean(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容器進行構造器注入:

//register
var  container = new WindsorContainer();
container.Register(Component.For<IParser>().ImplementedBy<Parser>());
container.Register(Component.For<IWriter>().ImplementedBy<Writer>());
container.Register(Component.For<Application>());

//resolve
var application = container.Resolve<Application>();
application.Execute("hel--lo, wor--ld");

//release
container.Release(application);

這個例子向我們展示了一個最簡單的依賴注入使用方式,register所有組件,resolve客戶端程序,最後的release步驟向我們展示瞭如果顯示從DI容器得到一個對象,應該顯示釋放該組件。這一步在大多數情況下並不是必須的,但是在特定場景下會發生內存泄漏

2、.net平臺下依賴注入最佳實踐

下面的解決方案描述了一個典型的應用程序分層結構,該分層結構用來描述如何使用Catle windsor進行依賴注入,注意:這並不是一個合理的領域驅動案例,例如我將Domain模型引用到了Application或者ApplicationService程序集中。
bestPractice
處在項目最底層的Repository程序集定義了一組UserRepository及其接口IUserRepository,這樣的一個組件如何註冊在Windsor Container中呢?Castle提供了一種叫做WindsorInstaller的機制:

public class RepositoryInstaller:IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Component.For<IUserRepository>().ImplementedBy<UserRepository>().LifestyleScoped());
    }
}

該Installer利用Fluent Api定義了IUserRepositoryUserRepository的對應關係,相對於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容器中。
比如UserApplicationServiceIUserApplicationService,以及未來將要實現的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容器。該實例展示瞭如何正確的使用依賴注入框架:

  1. 不同的程序集之間通過接口依賴,符合DIP原則;
  2. 通過依賴注入的方式定義好了可運行的流程,但是客戶端可以註冊不同的組件到DI容器中,符合IoC的思想;

3、如何在asp.net mvc和asp.net webapi使用依賴注入

本文提供的源碼中所含的WebApplicationSample項目演示瞭如何通過自定義WindsorControllerFactory來實現mvc的依賴注入,通過自定義WindsorCompositionRoot實現web api的依賴注入。

五、高級進階

asp.net core實現了一個還算簡單的DI容器DenpendencyInjection,感興趣的同學可以閱讀其源碼。

六、源碼下載

本文所描述的案例提供下載,點擊下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章