为什么我们需要框架来做依赖解析程序
本文关键字:依赖 程序 我们 框架 为什么 | 更新日期: 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
而不是ProductRepository
到ProductsController
注入到已经部署到客户端站点的软件中。
使用Spring.Net,您只需更新Spring配置文件(xml)即可使用CustomProductRepository
。因此,这样一来,您就可以避免在客户端重新编译和安装软件,因为您没有修改任何代码。