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("@","");
    }

    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,感兴趣的同学可以阅读其源码。

六、源码下载

本文所描述的案例提供下载,点击下载

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


引言 本文从Linux小白的视角, 在CentOS 7.x服务器上搭建一个Nginx-Powered AspNet Core Web准生产应用。 在开始之前,我们还是重温一下部署原理,正如你所常见的.Net Core 部署图: 在Linux上部署.Net Core App最好的方式是在Linux机器
引言: 多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类和await/async异步编程语法糖简化了异步编程方式。 相信很多开发者都看到如下异步编程实践原则: 遵守以上冷冰冰的②③条的原则,可保证异步程序按照预期状态正常运作;我们在各大编程论坛常看到违背
一. 宏观概念 ASP.NET Core Middleware是在应用程序处理管道pipeline中用于处理请求和操作响应的组件。 每个组件是pipeline 中的一环。 自行决定是否将请求传递给下一个组件 在处理管道的下个组件执行之前和之后执行业务逻辑 二. 特性和行为 ASP.NET Core处
背景 在.Net和C#中运行异步代码相当简单,因为我们有时候需要取消正在进行的异步操作,通过本文,可以掌握 通过CancellationToken取消任务(包括non-cancellable任务)。 Task&#160;表示无返回值的异步操作, 泛型版本Task&lt;TResult&gt;表示有返
HTTP基本认证 在HTTP中,HTTP基本认证(Basic Authentication)是一种允许网页浏览器或其他客户端程序以(用户名:口令) 请求资源的身份验证方式,不要求cookie,session identifier、login page等标记或载体。 - 所有浏览器据支持HTTP基本认
1.Linq 执行多列排序 OrderBy的意义是按照指定顺序排序,连续两次OrderBy,后面一个有可能会打乱前面一个的排序顺序,可能与预期不符。 要实现sql中的order by word,name类似效果; LINQ 有ThenBy可以紧接使用, ThenBy记住原本排序的值,然后再排其他值,
ASP.NET Core 核心特性:开源、跨平台、高性能是其决战JAVA的必胜法宝,最引人关注的跨平台特性 到底是怎么实现? &#xA; 本文分Unix、Windows剖析跨平台内幕,读完让你大呼过瘾。
前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现。 IAsyncResult BeginGetResponse(AsyncCallback callback, object state
引言 最近在公司开发了一个项目,项目部署架构图如下: 思路 如图中文本所述,公司大数据集群不允许直接访问外网,需要一个网关服务器代理请求,本处服务器A就是边缘代理服务器的作用。 通常技术人员最快捷的思路是在服务器A上部署IISʺpplication Request Routing Module组件
作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用Windows计划任务/wndows service程序/Crontab程序等主机方法在主机上部署定时任务程序/脚本。 但是很多时候,若使用的是共享主机或者受控主机,这些主机不允许你私自安装exe程序、Windows服务程序
引言 熟悉TPL Dataflow博文的朋友可能记得这是个单体程序,使用TPL Dataflow 处理工作流任务, 在使用Docker部署的过程中, 有一个问题一直无法回避: 在单体程序部署的瞬间(服务不可用)会有少量流量无法处理;更糟糕的情况下,迭代部署的这个版本有问题,上线后无法运作, 更多的流
合格的web后端程序员,除搬砖技能,还必须会给各种web服务器配置Https,本文结合ASP.NET Core部署模型聊一聊启用Https的方式。 温故知新 目前常见的Http请求明文传输,请求可能被篡改,访问的站点可能被伪造。 HTTPS是HTTP加上TLS/SSL协议构建的可进行加密传输、身份认
长话短说 前文《解剖HttpClientFactory,自由扩展HttpMessageHandler》主要讲如何为HttpClientFactory自定义HttpMessageHandler组件, 现在来完成课后的小作业: 将重点日志字段显示到Nlog的LayoutRenderer上。 本文实现一个
引言问题 作为资深老鸟,有事没事,出去面试;找准差距、定位价值。 面试必谈哈希, Q1:什么是哈希? Q2:哈希为什么快? Q3:你是怎么理解哈希算法利用空间换取时间的? Q4:你是怎么解决哈希冲突的? Q5:你有实际用写过哈希算法吗? 知识储备 哈希(也叫散列)是一种查找算法(可用于插入),哈希算
前言 如题,有感于博客园最近多次翻车,感觉像胡子眉毛一把抓, 定位不了生产环境的问题。 抛开流程问题,思考在生产环境中如何做故障排除,&#160;发现博客园里面这方面的文章比较少。 .Net 本身是提供了sos.dll工具帮助我们在生产中故障排除,通过提供有关内部公共语言运行时(CLR)环境的信息,
.NET程序是基于.NET Framework、.NET Core、Mono、【.NET实现】开发和运行的 ,定义以上【.NET实现】的标准规范称为.NET Standard .NET Standard .NET标准是一组API集合,由上层三种【.NET实现】的Basic Class Library
长话短说 上个月公司上线了一个物联网数据科学项目,我主要负责前端接受物联网事件,并提供 参数下载。 webapp 部署在Azure云上,参数使用Azure SQL Server存储。 最近从灰度测试转向全量部署之后,日志时常收到: SQL Session超限报错。 排查 我在Azure上使用的是 S
临近年关,搜狗,360浏览器出现页面无法成功跳转,同域Cookie丢失? 也许是服务端 SameSite惹的祸。&#xA;本文揭示由于Chrome低版本内核不识别 SameSite= None, 引发的单点登录故障。
本文聊一聊TraceID的作用和一般组成,衍生出ASP. NETCore 单体和分布式程序中 TraceId 的使用方式
通过给 HttpClint请求的日志增加 TraceId,解锁自定义扩展 HttpClientFacroty 的姿势