为什么我们需要框架来做依赖解析程序

本文关键字:依赖 程序 我们 框架 为什么 | 更新日期: 2023-09-27 18:20:31

我总是看到人们总是在谈论使用像Ninject、Unity、Windsor这样的框架来进行依赖解析和注入。以以下代码为例:

public class ProductsController : ApiController
{
    private IProductRepository _repository;
    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }
} 

我的问题是:为什么我们不能简单地写为:

public class ProductsController : ApiController
{
    private IProductRepository _repository;
    public ProductsController() :this(null)
    {}
    public ProductsController(IProductRepository repository)  
    {
        _repository = repository?? new ProductRepository();
    }
} 

在这种情况下,我们似乎不需要任何框架,即使是对于我们可以轻松模拟的单元测试。

那么,这些框架的真正目的是什么呢?

提前感谢!

为什么我们需要框架来做依赖解析程序

在这种情况下,您的ProductsController仍然依赖于一个低级别组件(在您的情况下是具体的ProductRepository),这违反了依赖反转原则。这是否是一个问题取决于多个因素,但它会导致以下问题:

  • ProductRepository的创建在整个应用程序中仍然是重复的,这导致当ProductRepository的构造函数有可能(假设ProductRepository在更多地方使用,这是非常合理的)违反开放/封闭原则时,您在整个应用程序中进行彻底的更改
  • 每当你决定用一个装饰器或拦截器来包装这个ProductService时,它会让你做出彻底的更改,因为它会增加交叉问题(如日志记录、审计跟踪、安全过滤等),你肯定不想在所有存储库中重复该代码(再次违反OCP)
  • 它强制ProductsController了解ProductsRepository可能是一个问题,这取决于您正在编写的应用程序的大小和复杂性

所以这不是关于框架的使用,而是关于应用软件设计原则。如果您决定坚持这些原则来提高应用程序的可维护性,那么像Ninject、Autofac和Simple Injector这样的框架可以帮助您提高应用程序启动路径的可维护度。但没有什么能阻止你在不使用任何工具或库的情况下应用这些原则。

小免责声明:我是狂热的Unity用户,这是我的2美分。

第一:违反SOLID(SRP/OCP/DIP)

@democodemonkey和@thumbmunkeys已经说过,您将这两个类紧密地结合在一起。假设一些类(假设它是ProductsThingamajigOne和ProductsThingamajigTwo)正在使用ProductsController,并使用其默认构造函数。如果架构师决定系统不应该使用将产品保存到文件中的ProductsDepository,而是应该使用数据库或云存储,该怎么办。会对课程产生什么影响?

第二:如果ProductRepository需要另一个依赖项怎么办

如果存储库基于数据库,则可能需要为其提供ConnectionString。如果它是基于文件的,你可能需要为它提供一类设置,提供保存文件的确切路径。事实是,通常情况下,应用程序往往包含依赖树(a依赖于B和C,B依赖于D,C依赖于E,D依赖于F和G,等等),它们的级别超过2级,因此SOLID违规更伤人,因为必须更改更多的代码才能执行某些任务,但即使在那之前,你能想象创建整个应用程序的代码吗?事实是,类可以有许多自己的依赖项——在这种情况下,前面描述的问题会成倍增加。

这通常是引导程序的工作——它定义依赖关系结构,并执行(通常)一个单一的解析,将整个系统启动,就像字符串上的木偶一样。

第三:如果依赖关系树不是一棵树,而是一张图,该怎么办

考虑以下情况:类A依赖于类B和C,B和C都依赖于类D,并且期望使用相同的D实例。一种常见的做法是将D设为单例,但这可能会导致很多问题。另一种选择是将D的实例传递到A的构造函数中,并让它创建B和C,或者将B和C的实例传递给A并在外部创建它们,复杂性会不断增加

第四:包装(组件)

您的代码假设"ProductsController"可以看到"ProductRepository"(汇编)。如果他们之间没有参考呢?组装映射可以是非平凡的。通常,引导代码(我假设它在代码中,而不是在配置文件中)是在引用整个解决方案的程序集中编写的。(这也是@Steven描述的)。

第五:你可以用IoC容器做一些很酷的事情

单身汉变得很容易(使用统一:注册时只需使用"containercontrolledlifetimemanager"),Lazy实例化变得非常容易(使用统一:寄存器映射和在构造函数中请求Func)。这些只是IoC容器(几乎)免费提供给您的东西的几个例子。

当然可以这样做,但这会导致以下问题:

  • IProductRepository的依赖关系不再是显式的,它看起来像是一个可选的依赖关系
  • 代码的其他部分可能会实例化IProductRepository的不同实现,在这种情况下这可能是个问题
  • 该类与ProductsController紧密耦合,因为它在内部创建了一个依赖项

在我看来,这不是一个框架问题。重点是通过在构造函数或属性中公开模块的依赖关系,使模块可组合。你的例子有点混淆了这一点。

如果类ProductRepository没有在与ProductsController相同的程序集中定义(或者如果您想将其移动到不同的程序集中),那么您刚刚引入了一个不想要的依赖项。

这是一种反模式,在Mark Seeman的开创性著作《.Net中的依赖注入》中被描述为"混蛋注入"。

然而,如果ProductRepository总是与ProductsController在同一个组件中,并且它不依赖于ProductsController组件的其他部分所依赖的任何东西,那么它可能是local default——在这种情况下,它是可以的

从类名来看,我打赌不应该引入这样的依赖关系,而您正在考虑混蛋注入。

此处ProductsController负责创建ProductRepository

如果ProductRepository在其构造函数中需要一个额外的参数,会发生什么?那么ProductsController将不得不更改,这违反了SRP。

以及增加所有对象的复杂性。

同时弄不清楚调用者是否需要传递子对象,或者它是可选的?

主要目的是将对象创建与其使用或消耗解耦。对象的创建"通常"由工厂类负责。在您的情况下,工厂类将被设计为返回实现IProductRepository接口的类型的对象。

在一些框架中,比如在Sprint.Net中,工厂类实例化在配置中(即在app.config或web.config中)以声明方式编写的对象。因此,使程序完全独立于它需要创建的对象。这有时会非常强大。

区分依赖注入和控制反转是不一样的,这一点很重要。你可以使用依赖注入,而不需要像unity、ninject…这样的IOC框架。。。,手动执行注射,他们通常称之为穷人DI。

在我的博客中,我最近写了一篇关于这个问题的文章http://xurxodeveloper.blogspot.com.es/2014/09/Inyeccion-de-Dependencias-DI.html

回到您的例子,我看到了实现中的弱点。

1-产品控制器依赖于具体而非抽象,违反SOLID

2-如果接口和存储库位于不同的项目中,您将被迫引用存储库所在的项目

3-如果将来需要向构造函数添加一个参数,那么当控制器只是一个存储库客户端时,就必须修改它。

4-控制器和存储库可以为不同的程序员开发,控制器程序员必须知道如何创建存储库

考虑这个用例:

假设将来,如果您想将CustomProductRepository而不是ProductRepositoryProductsController注入到已经部署到客户端站点的软件中。

使用Spring.Net,您只需更新Spring配置文件(xml)即可使用CustomProductRepository。因此,这样一来,您就可以避免在客户端重新编译和安装软件,因为您没有修改任何代码。