www.2527.com_澳门新葡8455手机版_新京葡娱乐场网址_
做最好的网站

关于依赖注入IOC,浅谈依赖注入

2020-03-24 10:12 来源:未知

时间: 2019-12-26阅读: 57标签: 代码

  之前一直不明白依赖注入有什么好处,甚至觉得它是鸡肋,现在想想,当时真是可笑。

最近几天在看一本名为Dependency Injection in .NET 的书,主要讲了什么是依赖注入,使用依赖注入的优点,以及.NET平台上依赖注入的各种框架和用法。在这本书的开头,讲述了软件工程中的一个重要的理念就是关注分离(Separation of concern, SoC)。依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。

如果是第一次接触这个概念,可能会一时没有头绪,网上的各种解释可能会让你更加混乱,并觉得它没那么简单。其实依赖注入本身是单纯、简单的。

  这个想法正如同说接口是没有用处一样。

关于什么是依赖注入,在Stack Overflow上面有一个问题,如何向一个5岁的小孩解释依赖注入,其中得分最高的一个答案是:

简单来说,依赖注入是一种方式、方法或者说手段,是让被注入者和注入者之间建立关联的手段。

  当整个项目非常庞大,各个方法之间的调用非常复杂,那么,可以想象一下,假设说没有任何的分离模块的想法,各个关系非常的复杂,不便于维护以及查找bug等等。这个时候,就需要一个东西,去将这么多复杂的玩意分离开来,很喜欢的一句话:高内聚,低耦合。

“When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

依赖注入的目的是松耦合,是交互对象之间的松耦合。

  举个栗子,现在一个很简单的功能,可能只需要通过一些方法互相调用就可以实现,OK,那么随着需求的增多,现在添加了几个功能,那么,我们把相同的东西划分为一个类吧,类由此而生,同样,类似的,一层一层如下所示:

What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”

今天,小芯带来的文章主要描述了关于进行依赖注入(dependency injection,DI)的五大原则,具有强大的实用性。

方法-->类-->接口-->模块-->甚至把各个模块之间的关系抽象出来(比如IOC/DI)

映射到面向对象程序开发中就是:高层类(5岁小孩)应该依赖底层基础设施(家长)来提供必要的服务。

遵循这五大基本思想,在进行DI时,你就无需额外花功夫于编写代码啦,超方便。

 

编写松耦合的代码说起来很简单,但是实际上写着写着就变成了紧耦合。

一、保持简单的构造函数

 

使用例子来说明可能更简洁明了,首先来看看什么样的代码是紧耦合。

构造函数应该保持简单。类的构造函数不应该做任何工作——也就是说,除了检查null、创建可创建类和存储依赖项供以后使用之外,它们不应该做任何事情。

  总之,一个核心的思想就是,对于一个大项目来说,把所有的模块尽可能的多分几个,一个里面的东西太多的话,多分开几个写,一句话表达:高内聚,低耦合

1 不好的实现

编写松耦合代码的第一步,可能大家都熟悉,那就是对系统分层。比如下面的经典的三层架构。

Web前端 1

分完层和实现好是两件事情,并不是说分好层之后就能够松耦合了。

它们不应该包含任何编码逻辑。一个类构造函数中没有检查null的if子句,那么这个类就会被分成两个类。(有不涉及if语句检查nil-value参数的方法。)

 

1.1 紧耦合的代码

有很多种方式来设计一个灵活的,可维护的复杂应用,但是n层架构是一种大家比较熟悉的方式,这里面的挑战在于如何正确的实现n层架构。

假设要实现一个很简单的电子商务网站,要列出商品列表,如下:

Web前端 2

下面就具体来演示通常的做法,是如何一步一步把代码写出紧耦合的。

复杂的构造函数表明类做的工作太多。保持构造函数简短、简单且无逻辑。

浅谈依赖注入

 

最近几天在看一本名为Dependency Injection in .NET 的书,主要讲了什么是依赖注入,使用依赖注入的优点,以及.NET平台上依赖注入的各种框架和用法。在这本书的开头,讲述了软件工程中的一个重要的理念就是关注分离(Separation of concern, SoC)。依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。

关于什么是依赖注入,在Stack Overflow上面有一个问题,如何向一个5岁的小孩解释依赖注入,其中得分最高的一个答案是:

“When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”

映射到面向对象程序开发中就是:高层类(5岁小孩)应该依赖底层基础设施(家长)来提供必要的服务。

编写松耦合的代码说起来很简单,但是实际上写着写着就变成了紧耦合。

使用例子来说明可能更简洁明了,首先来看看什么样的代码是紧耦合。

1.1.1 数据访问层

要实现商品列表这一功能,首先要编写数据访问层,需要设计数据库及表,在SQLServer中设计的数据库表Product结构如下:

Web前端 3

表设计好之后,就可以开始写代码了。在Visual Studio 中,新建一个名为DataAccessLayer的工程,添加一个ADO.NET Entity Data Model,此时Visual Studio的向导会自动帮我们生成Product实体和ObjectContext DB操作上下文。这样我们的 Data Access Layer就写好了。

Web前端 4

二、不要假设接口是抽象的

1 不好的实现

编写松耦合代码的第一步,可能大家都熟悉,那就是对系统分层。比如下面的经典的三层架构。

Web前端 5

分完层和实现好是两件事情,并不是说分好层之后就能够松耦合了。

1.1.2 业务逻辑层

表现层实际上可以直接访问数据访问层,通过ObjectContext 获取Product 列表。但是大多数情况下,我们不是直接把DB里面的数据展现出来,而是需要对数据进行处理,比如对会员,需要对某些商品的价格打折。这样我们就需要业务逻辑层,来处理这些与具体业务逻辑相关的事情。

新建一个类库,命名为DomainLogic,然后添加一个名为ProductService的类:

public class ProductService {
    private readonly CommerceObjectContext objectContext;

    public ProductService()
    {
        this.objectContext = new CommerceObjectContext();
    }

    public IEnumerable<Product> GetFeaturedProducts(
        bool isCustomerPreferred)
    {
        var discount = isCustomerPreferred ? .95m : 1;
        var products = (from p in this.objectContext
                            .Products
                        where p.IsFeatured
                        select p).AsEnumerable();
        return from p in products
                select new Product
                {
                    ProductId = p.ProductId,
                    Name = p.Name,
                    Description = p.Description,
                    IsFeatured = p.IsFeatured,
                    UnitPrice = p.UnitPrice * discount
                };
    }
}

现在我们的业务逻辑层已经实现了。

接口很不错,我一直对它赞不绝口。然而,重要的是要认识到并非每个接口都是抽象的。

1.1 紧耦合的代码

有很多种方式来设计一个灵活的,可维护的复杂应用,但是n层架构是一种大家比较熟悉的方式,这里面的挑战在于如何正确的实现n层架构。

假设要实现一个很简单的电子商务网站,要列出商品列表,如下:

Web前端 6

下面就具体来演示通常的做法,是如何一步一步把代码写出紧耦合的。

1.1.3 表现层

现在实现表现层逻辑,这里使用ASP.NET MVC,在Index 页面的Controller中,获取商品列表然后将数据返回给View。

public ViewResult Index()
{
    bool isPreferredCustomer = 
        this.User.IsInRole("PreferredCustomer");

    var service = new ProductService();
    var products = 
        service.GetFeaturedProducts(isPreferredCustomer);
    this.ViewData["Products"] = products;

    return this.View();
}

然后在View中将Controller中返回的数据展现出来:

<h2>Featured Products</h2>
<div>
<% var products =
        (IEnumerable<Product>)this.ViewData["Products"];
    foreach (var product in products)
    { %>
    <div>
    <%= this.Html.Encode(product.Name) %>
    (<%= this.Html.Encode(product.UnitPrice.ToString("C")) %>)
    </div>
<% } %>
</div>

Web前端,例如,如果接口是公共部分类的精确表示,实际上并没有抽象任何东西,对吗? (这些接口被称为头接口,类似于c 的头文件)。从类中提取的接口可以很容易地单独与该类紧密耦合,使得接口作为抽象类毫无用处。

1.1.1 数据访问层

要实现商品列表这一功能,首先要编写数据访问层,需要设计数据库及表,在SQLServer中设计的数据库表Product结构如下:

Web前端 7

表设计好之后,就可以开始写代码了。在Visual Studio 中,新建一个名为DataAccessLayer的工程,添加一个ADO.NET Entity Data Model,此时Visual Studio的向导会自动帮我们生成Product实体和ObjectContext DB操作上下文。这样我们的 Data Access Layer就写好了。

Web前端 8

1.2 分析

现在,按照三层“架构”我们的代码写好了,并且也达到了要求。整个项目的结构如下图:

 Web前端 9

这应该是我们通常经常写的所谓的三层架构。在Visual Studio中,三层之间的依赖可以通过项目引用表现出来。

最后,抽象类可能是有漏洞的——也就是说,它们可以揭示关于实现类的特定细节。有漏洞的抽象类通常也与特定的实现类绑定在一起。

1.1.2 业务逻辑层

表现层实际上可以直接访问数据访问层,通过ObjectContext 获取Product 列表。但是大多数情况下,我们不是直接把DB里面的数据展现出来,而是需要对数据进行处理,比如对会员,需要对某些商品的价格打折。这样我们就需要业务逻辑层,来处理这些与具体业务逻辑相关的事情。

新建一个类库,命名为DomainLogic,然后添加一个名为ProductService的类:

public class ProductService {
    private readonly CommerceObjectContext objectContext;

    public ProductService()
    {
        this.objectContext = new CommerceObjectContext();
    }

    public IEnumerable<Product> GetFeaturedProducts(
        bool isCustomerPreferred)
    {
        var discount = isCustomerPreferred ? .95m : 1;
        var products = (from p in this.objectContext
                            .Products
                        where p.IsFeatured
                        select p).AsEnumerable();
        return from p in products
                select new Product
                {
                    ProductId = p.ProductId,
                    Name = p.Name,
                    Description = p.Description,
                    IsFeatured = p.IsFeatured,
                    UnitPrice = p.UnitPrice * discount
                };
    }
}

现在我们的业务逻辑层已经实现了。

1.2.1 依赖关系图

现在我们来分析一下,这三层之间的依赖关系,很明显,上面的实现中,DomianLogic需要依赖SqlDataAccess,因为DomainLogic中用到了Product这一实体,而这个实体是定义在DataAccess这一层的。WebUI这一层需要依赖DomainLogic,因为ProductService在这一层,同时,还需要依赖DataAccess,因为在UI中也用到了Product实体,现在整个系统的依赖关系是这样的:

Web前端 10

三、不要对实现类做任何假设

1.1.3 表现层

现在实现表现层逻辑,这里使用ASP.NET MVC,在Index 页面的Controller中,获取商品列表然后将数据返回给View。

public ViewResult Index()
{
    bool isPreferredCustomer = 
        this.User.IsInRole("PreferredCustomer");

    var service = new ProductService();
    var products = 
        service.GetFeaturedProducts(isPreferredCustomer);
    this.ViewData["Products"] = products;

    return this.View();
}

然后在View中将Controller中返回的数据展现出来:

<h2>Featured Products</h2>
<div>
<% var products =
        (IEnumerable<Product>)this.ViewData["Products"];
    foreach (var product in products)
    { %>
    <div>
    <%= this.Html.Encode(product.Name) %>
    (<%= this.Html.Encode(product.UnitPrice.ToString("C")) %>)
    </div>
<% } %>
</div>

1.2.2 耦合性分析

使用三层结构的主要目的是分离关注点,当然还有一个原因是可测试性。我们应该将领域模型从数据访问层和表现层中分离出来,这样这两个层的变化才不会污染领域模型。在大的系统中,这点很重要,这样才能将系统中的不同部分隔离开来。

现在来看之前的实现中,有没有模块性,有没有那个模块可以隔离出来呢。现在添加几个新的case来看,系统是否能够响应这些需求:

添加新的用户界面

除了WebForm用户之外,可能还需要一个WinForm的界面,现在我们能否复用领域层和数据访问层呢?从依赖图中可以看到,没有任何一个模块会依赖表现层,因此很容易实现这一点变化。我们只需要创建一个WPF的富客户端就可以。现在整个系统的依赖图如下:

Web前端 11

更换新的数据源

可能过了一段时间,需要把整个系统部署到云上,要使用其他的数据存储技术,比如Azure Table Storage Service。现在,整个访问数据的协议发生了变化,访问Azure Table Storage Service的方式是Http协议,而之前的大多数.NET 访问数据的方式都是基于ADO.NET 的方式。并且数据源的保存方式也发生了改变,之前是关系型数据库,现在变成了key-value型数据库。

Web前端 12 

由上面的依赖关系图可以看出,所有的层都依赖了数据访问层,如果修改数据访问层,则领域逻辑层,和表现层都需要进行相应的修改。

当然,如果没有实现类,接口是毫无用处的。但是,作为开发人员不应对实现类做任何假设。

1.2 分析

现在,按照三层“架构”我们的代码写好了,并且也达到了要求。整个项目的结构如下图:

 Web前端 13

这应该是我们通常经常写的所谓的三层架构。在Visual Studio中,三层之间的依赖可以通过项目引用表现出来。

1.2.3 问题

除了上面的各层之间耦合下过强之外,代码中还有其他问题。

  • 领域模型似乎都写到了数据访问层中。所以领域模型看起来依赖了数据访问层。在数据访问层中定义了名为Product的类,这种类应该是属于领域模型层的。
  • 表现层中掺入了决定某个用户是否是会员的逻辑。这种业务逻辑应该是 业务逻辑层中应该处理的,所以也应该放到领域模型层
  • ProductService因为依赖了数据访问层,所以也会依赖在web.config 中配置的数据库连接字符串等信息。这使得,整个业务逻辑层也需要依赖这些配置才能正常运行。
  • 在View中,包含了太多了函数性功能。他执行了强制类型转换,字符串格式化等操作,这些功能应该是在界面显示得模型中完成。

上面可能是我们大多数写代码时候的实现, UI界面层去依赖了数据访问层,有时候偷懒就直接引用了这一层,因为实体定义在里面了。业务逻辑层也是依赖数据访问层,直接在业务逻辑里面使用了数据访问层里面的实体。这样使得整个系统紧耦合,并且可测试性差。那现在我们看看,如何修改这样一个系统,使之达到松散耦合,从而提高可测试性呢?

只该根据接口生成的契约进行编码。你可能已经编写了实现,但不应该在考虑实现的情况下针对接口编写代码。换句话说,针对接口的代码就好像一个全新的、更好的接口实现。

1.2.1 依赖关系图

现在我们来分析一下,这三层之间的依赖关系,很明显,上面的实现中,DomianLogic需要依赖SqlDataAccess,因为DomainLogic中用到了Product这一实体,而这个实体是定义在DataAccess这一层的。WebUI这一层需要依赖DomainLogic,因为ProductService在这一层,同时,还需要依赖DataAccess,因为在UI中也用到了Product实体,现在整个系统的依赖关系是这样的:

Web前端 14

2 较好的实现

依赖注入能够较好的解决上面出现的问题,现在可以使用这一思想来重新实现前面的系统。之所以重新实现是因为,前面的实现在一开始的似乎就没有考虑到扩展性和松耦合,使用重构的方式很难达到理想的效果。对于小的系统来说可能还可以,但是对于一个大型的系统,应该是比较困难的。

在写代码的时候,要管理好依赖性,在前面的实现这种,代码直接控制了依赖性:当ProductService需要一个ObjectContext类的似乎,直接new了一个,当HomeController需要一个ProductService的时候,直接new了一个,这样看起来很酷很方便,实际上使得整个系统具有很大的局限性,变得紧耦合。new 操作实际上就引入了依赖, 控制反转这种思想就是要使的我们比较好的管理依赖。

一个设计良好的接口会告诉你需要做什么以及如何使用它。该接口的实现对你使用该接口是无关紧要的。

1.2.2 耦合性分析

使用三层结构的主要目的是分离关注点,当然还有一个原因是可测试性。我们应该将领域模型从数据访问层和表现层中分离出来,这样这两个层的变化才不会污染领域模型。在大的系统中,这点很重要,这样才能将系统中的不同部分隔离开来。

现在来看之前的实现中,有没有模块性,有没有那个模块可以隔离出来呢。现在添加几个新的case来看,系统是否能够响应这些需求:

添加新的用户界面

除了WebForm用户之外,可能还需要一个WinForm的界面,现在我们能否复用领域层和数据访问层呢?从依赖图中可以看到,没有任何一个模块会依赖表现层,因此很容易实现这一点变化。我们只需要创建一个WPF的富客户端就可以。现在整个系统的依赖图如下:

Web前端 15

更换新的数据源

可能过了一段时间,需要把整个系统部署到云上,要使用其他的数据存储技术,比如Azure Table Storage Service。现在,整个访问数据的协议发生了变化,访问Azure Table Storage Service的方式是Http协议,而之前的大多数.NET 访问数据的方式都是基于ADO.NET 的方式。并且数据源的保存方式也发生了改变,之前是关系型数据库,现在变成了key-value型数据库。

Web前端 16 

由上面的依赖关系图可以看出,所有的层都依赖了数据访问层,如果修改数据访问层,则领域逻辑层,和表现层都需要进行相应的修改。

2.1 松耦合的代码

四、针对抽象类而非实现类的代码

1.2.3 问题

除了上面的各层之间耦合下过强之外,代码中还有其他问题。

  • 领域模型似乎都写到了数据访问层中。所以领域模型看起来依赖了数据访问层。在数据访问层中定义了名为Product的类,这种类应该是属于领域模型层的。
  • 表现层中掺入了决定某个用户是否是会员的逻辑。这种业务逻辑应该是 业务逻辑层中应该处理的,所以也应该放到领域模型层
  • ProductService因为依赖了数据访问层,所以也会依赖在web.config 中配置的数据库连接字符串等信息。这使得,整个业务逻辑层也需要依赖这些配置才能正常运行。
  • 在View中,包含了太多了函数性功能。他执行了强制类型转换,字符串格式化等操作,这些功能应该是在界面显示得模型中完成。

上面可能是我们大多数写代码时候的实现, UI界面层去依赖了数据访问层,有时候偷懒就直接引用了这一层,因为实体定义在里面了。业务逻辑层也是依赖数据访问层,直接在业务逻辑里面使用了数据访问层里面的实体。这样使得整个系统紧耦合,并且可测试性差。那现在我们看看,如何修改这样一个系统,使之达到松散耦合,从而提高可测试性呢?

2.1.1 表现层

首先从表现层来分析,表现层主要是用来对数据进行展现,不应该包含过多的逻辑。在Index的View页面中,代码希望可以写成这样

<h2>
    Featured Products</h2>
<div>
    <% foreach (var product in this.Model.Products)
        { %>
    <div>
        <%= this.Html.Encode(product.SummaryText) %></div>
    <% } %>
</div>

可以看出,跟之前的表现层代码相比,要整洁很多。很明显是不需要进行类型转换,要实现这样的目的,只需要让Index.aspx这个视图继承自 System.Web.Mvc.ViewPage<FeaturedProductsViewModel> 即可,当我们在从Controller创建View的时候,可以进行选择,然后会自动生成。整个用于展示的信息放在了SummaryText字段中。

这里就引入了一个视图模型(View-Specific Models),他封装了视图的行为,这些模型只是简单的POCOs对象(Plain Old CLR Objects)。FeatureProductsViewModel中包含了一个List列表,每个元素是一个ProductViewModel类,其中定义了一些简单的用于数据展示的字段。

Web前端 17

现在在Controller中,我们只需要给View返回FeatureProductsViewModel对象即可。比如:

public ViewResult Index()
{
    var vm = new FeaturedProductsViewModel();
    return View(vm);
}

现在返回的是空列表,具体的填充方式在领域模型中,我们接着看领域模型层。

该短语出自“四人帮”之一的埃里希·伽玛(Erich Gamma)(《设计模式》一书的作者),是一个重要的想法。如果只能教给新的开发者一件事,那就是这句格言。

2 较好的实现

依赖注入能够较好的解决上面出现的问题,现在可以使用这一思想来重新实现前面的系统。之所以重新实现是因为,前面的实现在一开始的似乎就没有考虑到扩展性和松耦合,使用重构的方式很难达到理想的效果。对于小的系统来说可能还可以,但是对于一个大型的系统,应该是比较困难的。

在写代码的时候,要管理好依赖性,在前面的实现这种,代码直接控制了依赖性:当ProductService需要一个ObjectContext类的似乎,直接new了一个,当HomeController需要一个ProductService的时候,直接new了一个,这样看起来很酷很方便,实际上使得整个系统具有很大的局限性,变得紧耦合。new 操作实际上就引入了依赖, 控制反转这种思想就是要使的我们比较好的管理依赖。

2.1.2 领域逻辑层

新建一个类库,这里面包含POCOs和一些抽象类型。POCOs用来对领域建模,抽象类型提供抽象作为到达领域模型的入口。依赖注入的原则是面向接口而不是具体的类编程,使得我们可以替换具体实现。

现在我们需要为表现层提供数据。因此用户界面层需要引用领域模型层。对数据访问层的简单抽象可以采用Patterns of Enterprise Application Architecture一书中讲到的Repository模式。因此定义一个ProductRepository抽象类,注意是抽象类,在领域模型库中。它定义了一个获取所有特价商品的抽象方法:

public abstract class ProductRepository
{
    public abstract IEnumerable<Product> GetFeaturedProducts();
}

这个方法的Product类中只定义了商品的基本信息比如名称和单价。整个关系图如下:

Web前端 18

现在来看表现层,HomeController中的Index方法应该要使用ProductService实例类来获取商品列表,执行价格打折,并且把Product类似转化为ProductViewModel实例,并将该实例加入到FeaturesProductsViewModel中。因为ProductService有一个带有类型为ProductReposity抽象类的构造函数,所以这里可以通过构造函数注入实现了ProductReposity抽象类的实例。这里和之前的最大区别是,我们没有使用new关键字来立即new一个对象,而是通过构造函数的方式传入具体的实现。

现在来看表现层代码:

public partial class HomeController : Controller
{
    private readonly ProductRepository repository;

    public HomeController(ProductRepository repository)
    {
        if (repository == null)
        {
            throw new ArgumentNullException("repository");
        }

        this.repository = repository;
    }

    public ViewResult Index()
    {
        var productService = new ProductService(this.repository);

        var vm = new FeaturedProductsViewModel();

        var products = productService.GetFeaturedProducts(this.User);
        foreach (var product in products)
        {
            var productVM = new ProductViewModel(product);
            vm.Products.Add(productVM);
        }

        return View(vm);
    }

}

在HomeController的构造函数中,传入了实现了ProductRepository抽象类的一个实例,然后将该实例保存在定义的私有的只读的ProductRepository类型的repository对象中,这就是典型的通过构造函数注入。在Index方法中,获取数据的ProductService类中的主要功能,实际上是通过传入的repository类来代理完成的。

ProductService类是一个纯粹的领域对象,实现如下:

public class ProductService
{
    private readonly ProductRepository repository;

    public ProductService(ProductRepository repository)
    {
        if (repository == null)
        {
            throw new ArgumentNullException("repository");
        }

        this.repository = repository;
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts(IPrincipal user)
    {
        if (user == null)
        {
            throw new ArgumentNullException("user");
        }

        return from p in
                        this.repository.GetFeaturedProducts()
                select p.ApplyDiscountFor(user);
    }
}

可以看到ProductService也是通过构造函数注入的方式,保存了实现了ProductReposity抽象类的实例,然后借助该实例中的GetFeatureProducts方法,获取原始列表数据,然后进行打折处理,进而实现了自己的GetFeaturedProducts方法。在该GetFeaturedProducts方法中,跟之前不同的地方在于,现在的参数是IPrincipal,而不是之前的bool型,因为判断用户的状况,这是一个业务逻辑,不应该在表现层处理。IPrincipal是BCL中的类型,所以不存在额外的依赖。我们应该基于接口编程IPrincipal是应用程序用户的一种标准方式。

这里将IPrincipal作为参数传递给某个方法,然后再里面调用实现的方式是依赖注入中的方法注入的手段。和构造函数注入一样,同样是将内部实现代理给了传入的依赖对象。

现在我们只剩下两块地方没有处理了:

  • 没有ProductRepository的具体实现,这个很容易实现,后面放到数据访问层里面去处理,我们只需要创建一个具体的实现了ProductRepository的数据访问类即可。
  • 默认上,ASP.NET MVC 希望Controller对象有自己的默认构造函数,因为我们在HomeController中添加了新的构造函数来注入依赖,所以MVC框架不知道如何解决创建实例,因为有依赖。这个问题可以通过开发一个IControllerFactory来解决,该对象可以创建一个具体的ProductRepositry实例,然后传给HomeController这里不多讲。

现在我们的领域逻辑层已经写好了。在该层,我们只操作领域模型对象,以及.NET BCL 中的基本对象。模型使用POCOs来表示,命名为Product。领域模型层必须能够和外界进行交流(database),所以需要一个抽象类(Repository)来时完成这一功能,并且在必要的时候,可以替换具体实现。

抽象类是灵活的——通常是接口但不总是(见下文)。

2.1 松耦合的代码

2.1.3 数据访问层

现在我们可以使用LINQ to Entity来实现具体的数据访问层逻辑了。因为要实现领域模型的ProductRepository抽象类,所以需要引入领域模型层。注意,这里的依赖变成了数据访问层依赖领域模型层。跟之前的恰好相反,代码实现如下:

public class SqlProductRepository : Domain.ProductRepository
{
    private readonly CommerceObjectContext context;

    public SqlProductRepository(string connString)
    {
        this.context =
            new CommerceObjectContext(connString);
    }

    public override IEnumerable<Domain.Product> GetFeaturedProducts()
    {
        var products = (from p in this.context.Products
                        where p.IsFeatured
                        select p).AsEnumerable();
        return from p in products
                select p.ToDomainProduct();
    }
}

在这里需要注意的是,在领域模型层中,我们定义了一个名为Product的领域模型,然后再数据访问层中Entity Framework帮我们也生成了一个名为Product的数据访问层实体,他是和db中的Product表一一对应的。所以我们在方法返回的时候,需要把类型从db中的Product转换为领域模型中的POCOs Product对象。

Web前端 19 

Domain Model中的Product是一个POCOs类型的对象,他仅仅包含领域模型中需要用到的一些基本字段,DataAccess中的Product对象是映射到DB中的实体,它包含数据库中Product表定义的所有字段,在数据表现层中我们 定义了一个ProductViewModel数据展现的Model。

这两个对象之间的转换很简单:

public class Product
{
    public Domain.Product ToDomainProduct()
    {
        Domain.Product p = new Domain.Product();
        p.Name = this.Name;
        p.UnitPrice = this.UnitPrice;
        return p;
    }
}

接口(或抽象类)可以通过多种方式实现。可以在实现完成前对接口进行编码。如果对实现进行编码,将创建一个紧密耦合且不灵活的系统。不要把自己限定在单一的实现中。相反,使用抽象,编出可扩展、可重用及灵活的代码。

2.1.1 表现层

首先从表现层来分析,表现层主要是用来对数据进行展现,不应该包含过多的逻辑。在Index的View页面中,代码希望可以写成这样

<h2>
    Featured Products</h2>
<div>
    <% foreach (var product in this.Model.Products)
        { %>
    <div>
        <%= this.Html.Encode(product.SummaryText) %></div>
    <% } %>
</div>

可以看出,跟之前的表现层代码相比,要整洁很多。很明显是不需要进行类型转换,要实现这样的目的,只需要让Index.aspx这个视图继承自 System.Web.Mvc.ViewPage<FeaturedProductsViewModel> 即可,当我们在从Controller创建View的时候,可以进行选择,然后会自动生成。整个用于展示的信息放在了SummaryText字段中。

这里就引入了一个视图模型(View-Specific Models),他封装了视图的行为,这些模型只是简单的POCOs对象(Plain Old CLR Objects)。FeatureProductsViewModel中包含了一个List列表,每个元素是一个ProductViewModel类,其中定义了一些简单的用于数据展示的字段。

Web前端 20

现在在Controller中,我们只需要给View返回FeatureProductsViewModel对象即可。比如:

public ViewResult Index()
{
    var vm = new FeaturedProductsViewModel();
    return View(vm);
}

现在返回的是空列表,具体的填充方式在领域模型中,我们接着看领域模型层。

2.2 分析

五、永远不要创建不该创建的东西

2.1.2 领域逻辑层

新建一个类库,这里面包含POCOs和一些抽象类型。POCOs用来对领域建模,抽象类型提供抽象作为到达领域模型的入口。依赖注入的原则是面向接口而不是具体的类编程,使得我们可以替换具体实现。

现在我们需要为表现层提供数据。因此用户界面层需要引用领域模型层。对数据访问层的简单抽象可以采用Patterns of Enterprise Application Architecture一书中讲到的Repository模式。因此定义一个ProductRepository抽象类,注意是抽象类,在领域模型库中。它定义了一个获取所有特价商品的抽象方法:

public abstract class ProductRepository
{
    public abstract IEnumerable<Product> GetFeaturedProducts();
}

这个方法的Product类中只定义了商品的基本信息比如名称和单价。整个关系图如下:

Web前端 21

现在来看表现层,HomeController中的Index方法应该要使用ProductService实例类来获取商品列表,执行价格打折,并且把Product类似转化为ProductViewModel实例,并将该实例加入到FeaturesProductsViewModel中。因为ProductService有一个带有类型为ProductReposity抽象类的构造函数,所以这里可以通过构造函数注入实现了ProductReposity抽象类的实例。这里和之前的最大区别是,我们没有使用new关键字来立即new一个对象,而是通过构造函数的方式传入具体的实现。

现在来看表现层代码:

public partial class HomeController : Controller
{
    private readonly ProductRepository repository;

    public HomeController(ProductRepository repository)
    {
        if (repository == null)
        {
            throw new ArgumentNullException("repository");
        }

        this.repository = repository;
    }

    public ViewResult Index()
    {
        var productService = new ProductService(this.repository);

        var vm = new FeaturedProductsViewModel();

        var products = productService.GetFeaturedProducts(this.User);
        foreach (var product in products)
        {
            var productVM = new ProductViewModel(product);
            vm.Products.Add(productVM);
        }

        return View(vm);
    }

}

在HomeController的构造函数中,传入了实现了ProductRepository抽象类的一个实例,然后将该实例保存在定义的私有的只读的ProductRepository类型的repository对象中,这就是典型的通过构造函数注入。在Index方法中,获取数据的ProductService类中的主要功能,实际上是通过传入的repository类来代理完成的。

ProductService类是一个纯粹的领域对象,实现如下:

public class ProductService
{
    private readonly ProductRepository repository;

    public ProductService(ProductRepository repository)
    {
        if (repository == null)
        {
            throw new ArgumentNullException("repository");
        }

        this.repository = repository;
    }

    public IEnumerable<DiscountedProduct> GetFeaturedProducts(IPrincipal user)
    {
        if (user == null)
        {
            throw new ArgumentNullException("user");
        }

        return from p in
                        this.repository.GetFeaturedProducts()
                select p.ApplyDiscountFor(user);
    }
}

可以看到ProductService也是通过构造函数注入的方式,保存了实现了ProductReposity抽象类的实例,然后借助该实例中的GetFeatureProducts方法,获取原始列表数据,然后进行打折处理,进而实现了自己的GetFeaturedProducts方法。在该GetFeaturedProducts方法中,跟之前不同的地方在于,现在的参数是IPrincipal,而不是之前的bool型,因为判断用户的状况,这是一个业务逻辑,不应该在表现层处理。IPrincipal是BCL中的类型,所以不存在额外的依赖。我们应该基于接口编程IPrincipal是应用程序用户的一种标准方式。

这里将IPrincipal作为参数传递给某个方法,然后再里面调用实现的方式是依赖注入中的方法注入的手段。和构造函数注入一样,同样是将内部实现代理给了传入的依赖对象。

现在我们只剩下两块地方没有处理了:

  • 没有ProductRepository的具体实现,这个很容易实现,后面放到数据访问层里面去处理,我们只需要创建一个具体的实现了ProductRepository的数据访问类即可。
  • 默认上,ASP.NET MVC 希望Controller对象有自己的默认构造函数,因为我们在HomeController中添加了新的构造函数来注入依赖,所以MVC框架不知道如何解决创建实例,因为有依赖。这个问题可以通过开发一个IControllerFactory来解决,该对象可以创建一个具体的ProductRepositry实例,然后传给HomeController这里不多讲。

现在我们的领域逻辑层已经写好了。在该层,我们只操作领域模型对象,以及.NET BCL 中的基本对象。模型使用POCOs来表示,命名为Product。领域模型层必须能够和外界进行交流(database),所以需要一个抽象类(Repository)来时完成这一功能,并且在必要的时候,可以替换具体实现。

2.2.1 依赖关系图

现在,整个系统的依赖关系图如下:

Web前端 22

表现层和数据访问层都依赖领域模型层,这样,在前面的case中,如果我们新添加一个UI界面;更换一种数据源的存储和获取方式,只需要修改对应层的代码即可,领域模型层保持了稳定。

类别应该遵循单一职责原则——即一个类只做一件事。

2.1.3 数据访问层

现在我们可以使用LINQ to Entity来实现具体的数据访问层逻辑了。因为要实现领域模型的ProductRepository抽象类,所以需要引入领域模型层。注意,这里的依赖变成了数据访问层依赖领域模型层。跟之前的恰好相反,代码实现如下:

public class SqlProductRepository : Domain.ProductRepository
{
    private readonly CommerceObjectContext context;

    public SqlProductRepository(string connString)
    {
        this.context =
            new CommerceObjectContext(connString);
    }

    public override IEnumerable<Domain.Product> GetFeaturedProducts()
    {
        var products = (from p in this.context.Products
                        where p.IsFeatured
                        select p).AsEnumerable();
        return from p in products
                select p.ToDomainProduct();
    }
}

在这里需要注意的是,在领域模型层中,我们定义了一个名为Product的领域模型,然后再数据访问层中Entity Framework帮我们也生成了一个名为Product的数据访问层实体,他是和db中的Product表一一对应的。所以我们在方法返回的时候,需要把类型从db中的Product转换为领域模型中的POCOs Product对象。

Web前端 23 

Domain Model中的Product是一个POCOs类型的对象,他仅仅包含领域模型中需要用到的一些基本字段,DataAccess中的Product对象是映射到DB中的实体,它包含数据库中Product表定义的所有字段,在数据表现层中我们 定义了一个ProductViewModel数据展现的Model。

这两个对象之间的转换很简单:

public class Product
{
    public Domain.Product ToDomainProduct()
    {
        Domain.Product p = new Domain.Product();
        p.Name = this.Name;
        p.UnitPrice = this.UnitPrice;
        return p;
    }
}

2.2.2 时序图

整个系统的时序图如下:

Web前端 24

系统启动的时候,在Global.asax中创建了一个自定义了Controller工厂类,应用程序将其保存在本地便两种,当页面请求进来的时候,程序出发该工厂类的CreateController方法,并查找web.config中的数据库连接字符串,将其传递给新的SqlProductRepository实例,然后将SqlProductRepository实例注入到HomeControll中,并返回。

然后应用调用HomeController的实例方法Index来创建新的ProductService类,并通过构造函数传入SqlProductRepository。ProductService的GetFeaturedProducts 方法代理给SqlProductRepository实例去实现。

最后,返回填充好了FeaturedProductViewModel的ViewResult对象给页面,然后MVC进行合适的展现。

如此,就不应再创建事物,否则是做了两件事。相反,应根据其所需的功能创建和提供该功能。

2.2 分析

2.2.3 新的结构

在1.1的实现中,采用了三层架构,在改进后的实现中,在UI层和领域模型层中加入了一个表现模型(presentation model)层。如下图:

Web前端 25

 

将Controllers和ViewModel从表现层移到了表现模型层,仅仅将视图(.aspx和.ascx文件)和聚合根对象(Composition Root)保留在了表现层中。之所以这样处理,是可以使得尽可能的使得表现层能够可配置而其他部分尽可能的可以保持不变。

可创建类 vs 可注入类

2.2.1 依赖关系图

现在,整个系统的依赖关系图如下:

Web前端 26

表现层和数据访问层都依赖领域模型层,这样,在前面的case中,如果我们新添加一个UI界面;更换一种数据源的存储和获取方式,只需要修改对应层的代码即可,领域模型层保持了稳定。

3. 结语

一不小心我们就编写出了紧耦合的代码,有时候以为分层了就可以解决这一问题,但是大多数的时候,都没有正确的实现分层。之所以容易写出紧耦合的代码有一个原因是因为编程语言或者开发环境允许我们只要需要一个新的实例对象,就可以使用new关键字来实例化一个。如果我们需要添加依赖,Visual Studio有些时候可以自动帮我们添加引用。这使得我们很容易就犯错,使用new关键字,就可能会引入以来;添加引用就会产生依赖。

减少new引入的依赖及紧耦合最好的方式是使用构造函数注入依赖这种设计模式:即如果我们需要一个依赖的实例,通过构造函数注入。在第二个部分的实现演示了如何针对抽象而不是具体编程。

构造函数注入是反转控制的一个例子,因为我们反转了对依赖的控制。不是使用new关键字创建一个实例,而是将这种行为委托给了第三方实现。

希望本文能够给大家了解如何真正实现三层架构,编写松散耦合,可维护,可测试性的代码提供一些帮助。

 

 

那么应该创建什么呢?

2.2.2 时序图

整个系统的时序图如下:

Web前端 27

系统启动的时候,在Global.asax中创建了一个自定义了Controller工厂类,应用程序将其保存在本地便两种,当页面请求进来的时候,程序出发该工厂类的CreateController方法,并查找web.config中的数据库连接字符串,将其传递给新的SqlProductRepository实例,然后将SqlProductRepository实例注入到HomeControll中,并返回。

然后应用调用HomeController的实例方法Index来创建新的ProductService类,并通过构造函数传入SqlProductRepository。ProductService的GetFeaturedProducts 方法代理给SqlProductRepository实例去实现。

最后,返回填充好了FeaturedProductViewModel的ViewResult对象给页面,然后MVC进行合适的展现。

我们应该关注两种不同的对象:可创建类和可注入类。

2.2.3 新的结构

在1.1的实现中,采用了三层架构,在改进后的实现中,在UI层和领域模型层中加入了一个表现模型(presentation model)层。如下图:

Web前端 28

 

将Controllers和ViewModel从表现层移到了表现模型层,仅仅将视图(.aspx和.ascx文件)和聚合根对象(Composition Root)保留在了表现层中。之所以这样处理,是可以使得尽可能的使得表现层能够可配置而其他部分尽可能的可以保持不变。

可创建类指应该继续创建的类。是常见的运行库或实用程序类。

3. 结语

一不小心我们就编写出了紧耦合的代码,有时候以为分层了就可以解决这一问题,但是大多数的时候,都没有正确的实现分层。之所以容易写出紧耦合的代码有一个原因是因为编程语言或者开发环境允许我们只要需要一个新的实例对象,就可以使用new关键字来实例化一个。如果我们需要添加依赖,Visual Studio有些时候可以自动帮我们添加引用。这使得我们很容易就犯错,使用new关键字,就可能会引入以来;添加引用就会产生依赖。

减少new引入的依赖及紧耦合最好的方式是使用构造函数注入依赖这种设计模式:即如果我们需要一个依赖的实例,通过构造函数注入。在第二个部分的实现演示了如何针对抽象而不是具体编程。

构造函数注入是反转控制的一个例子,因为我们反转了对依赖的控制。不是使用new关键字创建一个实例,而是将这种行为委托给了第三方实现。

希望本文能够给大家了解如何真正实现三层架构,编写松散耦合,可维护,可测试性的代码提供一些帮助。

 

参考

通常运行库中的类被认为是可创建类。这些类应该被创建,而非注入。它们的使用期限很短,通常不超过一种方法的范围。可创建类在所有类中是必不可少的,可以在构造函数中创建。一个可创建类只能传递给构造函数的另一个类。

另一方面,可注入类是我们永远不想直接创建的类。也是不希望硬编码依赖项的类,应始终通过DI传递。

在构造函数中,它们通常被作为是依赖项。根据以上规则,可注入类应该依赖接口注入的类,而非实例。

它通常是作为业务逻辑而编写的类。隐藏在抽象类后,通常是接口。还要注意,可注入类可在构造函数中请求其他的可注入类。

适当使用DI对代码的意义重大。做好这件事并不难,但确实需要有远见、计划和设计。

但是,在维护代码时,小小的工作将带来大大的回报。修复松散耦合的代码是维护人员的梦想,我们要努力编写这样的代码,相信事后会收获满满。

原文:-Cxb8y1J5msxVk4-FA1Uw相关链接:-programming/five-principles-of-dependency-injection-5bd0cca9cb04

TAG标签:
版权声明:本文由澳门新葡8455手机版发布于Web前端,转载请注明出处:关于依赖注入IOC,浅谈依赖注入