3.3 依赖注入

3.3.1 依赖注入简介

通常情况下,应用程序是由多个组件构成的,而组件与组件之间往往存在依赖关系。这种依赖关系是指两个不同组件之间的引用关系,当其中一方不存在时,另一方就不能正常工作,甚至不能独立存在。例如,要在界面上显示一些结果,就需要调用类似如下的获取数据的组件:

public class DataService
{
    public List<Book> GetAllBooks();
    {
        //
        return data;
    }
}

对于上述场景,通常的做法是,在需要显示数据的地方(也就是依赖这个类的位置)将DataService实例化,然后调用GetAllBooks方法获取数据,并最终显示结果。然而,这种依赖方式会增加调用方和被调用方之间的耦合,这种耦合又会增加应用程序的维护成本及灵活性,同时它也增加了单元测试的难度。

要解决这一问题,就需要用到依赖倒置原则(Dependency Inversion Principle),这个原则指明,高层不应直接依赖低层,两者均依赖抽象(或接口),如图3-4所示。

图3-4 依赖倒置原则

因此,对于依赖DataService服务的地方,如果将它对DataService的依赖,替换为对接口(如IDataService)的依赖,则高层(即要使用DataService服务的地方)不再直接依赖低层,而是依赖于接口。此时,高层只需要关心接口,而不再需要关心具体的实现,并且,高层可以根据自身的需要来设计接口,并由低层实现该接口,因此形成了“依赖倒置”。

基于此,定义一个名为IDataService的接口,并使DataService实现此接口:

public interface IDataService
{
    List<Book> GetAllBooks();
}
public class DataService : IDataService
{
    public List<Book> GetAllBooks();
    {
        // 
    {
        //获取数据
        return data;
    }
}

对于要调用DataService的地方,可以这样修改:

public class DisplayDataService
{
    private readonly IDataService_dataService;
    public DisplayDataService(IDataService dataService)
    {
        this._dataService = dataService;
    }
    public void ShowData()
    {
        var data =_dataService.GetAllBooks();
        //显示数据
    }
}

接下来,只需要在实例化DisplayDataService类时,在它的构造函数中传入一个IDataService接口的具体实现即可,如下所示。

IDataService dataService = new DataService();
DisplayDataService displayService = new DisplayDataService(dataService);

同样,当要进行单元测试时,只要创建一个实现IDataSerivce接口的模拟类,就可以在单元测试中使用与实际环境不同的依赖项。可见使用“依赖倒置”原则解决了上述程序中高耦合及难以测试等问题。

在上面的例子中,通过构造函数向DataDisplayService注入了它所需要的依赖,这种注入依赖的方式称为构造函数注入,它也是最常见的方式。通过构造函数获得所需要的依赖后,就可以将它保存为类级别的全局变量,也就可以在整个类中使用。构造函数注入也遵循了显式依赖原则(Explicit Dependencies Principle)。

除了构造函数注入外,还有另外两种注入方式:属性注入和方法注入。属性注入是通过设置类的属性来获取所需要的依赖,它不像构造函数注入一样,只有显式地提供所有的依赖,才能创建指定的类;反之,只要在已经实例化的对象上设置相应的属性即可。但是,这样也会存在问题,由于为依赖项属性设置的值并非是强制的,因此很容易忘记设置,由此引发不必要的异常。

方法注入是通过在方法的参数中传入所需要的依赖,如果类中的某一个方法需要依赖其他组件,则可以增加相应的参数。同时,这个方法也应该为public类型。

当应用程序中有多处要用到依赖注入时,就需要一个专门的类来负责管理创建所需要的类并创建它所有可能要用到的依赖,这个类就是依赖注入容器(Dependency Injection Container),也可以称之为控制反转容器(Inversion of Control Container,IoC容器)。

我们可以把依赖注入容器看作一个用于创建对象的工厂,它负责向外提供被请求要创建的对象,当创建这个对象时,如果它又依赖了其他对象或服务,那么容器会负责在其内部查找需要的依赖,并创建这些依赖,直至所有的依赖项都创建完成后,最终返回被请求的对象。除了创建对象和它们的依赖之外,容器也负责管理所创建对象的生命周期。常见的依赖注入容器有Autofac和SimpleInjector等。