应用程序架构 - 在依赖注入之前先了解依赖






4.83/5 (22投票s)
本文将介绍依赖、依赖的类型、用法、优点和缺点;同时还将涵盖依赖倒置、控制反转和 IOC 框架。
依赖注入
软件设计主题的受众 - 开发者、应用程序架构师
您将从主题中学到什么
我知道您已经熟悉依赖倒置(DI)和控制反转(IOC)。所以,如果您已经知道了这些,但对将依赖注入到您的组件中不够自信;但您想重用您的组件并想了解组件的最佳设计实践,那么这篇文章适合您。如果您偏爱分层风格、N 层架构风格、洋葱或雏菊架构风格,并使用领域驱动设计或其他方法,那么您可能需要对相同类型组件的相同或不同应用程序进行不同的实现。因此,首先了解依赖及其类型和特征,然后注入依赖将非常容易,您也将准备好使用依赖倒置原则。
总之,本主题将涵盖以下概念
- 什么是依赖
- 依赖的类型
- 依赖的特征
- 在类设计中使用依赖
- 依赖倒置
- 在哪些场景下强制使用 DI
- 什么是 DI 和 IOC
- DI 的类型
- DI 的优点和缺点
- IOC 框架
- 在 Web 应用程序中使用 Munq
- 使用托管可扩展性框架 (MEF)
让我们深入了解基本概念
在 DI 中,什么是依赖?
不再是理论和定义,让我们来看一个例子。
在例子中
Customer
类需要 'CheckingAccount
' 和 'SavingAccount
' 类来完成其工作。Customer
类没有 'CheckingAccount
' 和 'SavingAccount
' 类就无法完成其工作。
因此:
Customer
类是 'CheckingAccount
' 和 'SavingAccount
' 类的**依赖项**。- '
CheckingAccount
' 和 'SavingAccount
' 类是Customer
类的**依赖**。
依赖的类关系
- 相互使用的两个类(
Customer
和SavingAccount
或CheckingAccount
)称为“耦合”。 - 类之间的耦合可以是松散的、紧密的,或介于两者之间。
紧密耦合的依赖
在示例中,CheckingAccount
和 SavingingAccount
与 Customer
类紧密耦合。此实现违反了“开闭原则”,该原则规定您的类应该对扩展开放,对修改关闭。无论如何,忘记这个原则。只要想想您的支票账户类在此设计中是固定的,如果您需要支票账户类的另一个实现,您该怎么办?
松耦合依赖
在示例中,接口 IBankAccount
的实现是 CheckingAccount
和 SavingAccount
,它们与 customer
类松耦合。您可以将 IBankAccount
的任何不同实现注入到 Customer
类中。
依赖的方向
- 依赖或耦合是方向性的。
Customer
依赖于CheckingAccount
和SavingingAccount
,但这并不意味着CheckingAccount
和SavingAccount
也依赖于Customer
。
依赖的特征
- 可见依赖
- 隐藏依赖
- 直接依赖
- 间接依赖
- 编译时依赖
- 运行时依赖
可见依赖
在示例中,接口 IBankAccount
的实现是 CheckingAccount
和 SavingingAccount
。如果要创建 customer
类的实例,您可以通过其构造函数注入 IBankAccount
的任何不同实现。这在对象创建时是可见的。
隐藏依赖
现在,如果您创建 Customer
类的对象,您将无法注入 IBankAccount
接口的任何实现。这在对象创建时是隐藏的。
直接依赖
在此示例中,Customer
类使用 CheckingAccount
和 SavingAccount
。这意味着 Customer
对 CheckingAccount
和 SavingAccount
有直接依赖。因此,这称为直接依赖。
间接依赖
在示例中,CheckingAccount
类与 DataAccessForChecking
有直接依赖。
因此,Customer
对 CheckingAccount
有直接依赖。而 CheckingAccount
对 DataAccessForChecking
有直接依赖。所以,Customer
类对 DataAccessForChecking
有间接依赖。
为了简单起见,我现在避免讨论编译时和运行时依赖。稍后,当我讨论 MEF 和 MUNQ 时,我将解释它们。
最后,我可以说改变你的思维方式,你就会意识到事实。
依赖倒置
看看上面我们正在考虑分层架构的例子。我将此架构作为示例。请注意,层和技术是两种不同的风格。我将忽略这部分,因为我的主要目标是向您展示 DI 和 IOC。
表示层(PL)可能包含用户界面层(UI)和表示逻辑层(PLL)。为了方便起见,假设我们有一个 Web 应用程序,UI 指的是 ASP.NET 的 Web Form(如 *.aspx)或 ASP.NET MVC 的视图(如 *.cshtml)。PLL 指的是 ASP.NET MVC 的控制器类或 ASP.NET 的 *.aspx.cs。
业务层(BL)可以是您的领域层。数据访问层(DAL)。您可以使用任何 ORM,如 Entity-Framework,作为 DAL。
假设您通过任何 Web 门户购买了一些产品。现在您想查看您的订单项。因此,当您单击一个按钮显示您的订单时,视图将与 `controller` 类通信。`Controller` 类将调用 BL,然后 BL 将调用 DAL 来验证客户账户信息,最后 DAL 将订单信息发送给 BL,BL 将其发送给 PL 以显示订单列表。
简而言之,各层的工作方式如下:
- PL 依赖于 BL
- BL 依赖于 DAL
因此,高层依赖于低层,这些是紧密耦合的层。我们无法在运行时更改这种依赖关系,或者如果我们有 DAL 或 BLL 的多个实现,会发生什么?我们能否从层中更改依赖关系的方式?
现实生活场景
为了方便理解,假设您有 DLL 的不同实现。例如,IDataAccess
有两个实现,分别是 DataAccessForSql
和 DataAccessForOracle
。根据 `customer` 的情况,有时您需要 DataAccessForSql
,有时您需要 DataAccessForOracle
。但紧密耦合的设计在这种情况下不会帮助您。
同样,您有多种支付系统,如 PayPal、Visa 和 MasterCard。您需要根据支付系统对业务逻辑有不同的实现。例如,假设 IPayment
有一些实现,如 PaymentForMasterCard
、PaymentForPayPal
和 PaymentForVisa
。因此,您需要根据客户的偏好在它们之间切换。紧密耦合的设计不会为您提供这些功能。
因此,解决方案是依赖倒置。
依赖倒置原则
它指出:“*高层不应依赖于低层。两者都应依赖于抽象*”。
因此,我们可以将 DAL 的任何实现注入 BL,将 BL 的任何实现注入 PL。
依赖注入
依赖注入是一种对象配置风格,其中对象由外部实体进行配置。
在哪些场景下需要 DI
- 除非您无法控制对象的生命周期,否则不要使用 DI。
- 除非有必要,否则不要使用容器。
- 如果您可以控制调用 new 来创建对象,那么您永远不需要 DI 容器。
依赖注入的类型
- 构造函数注入
- Setter 注入
- 接口注入
构造函数注入:构造函数注入使用参数注入依赖项。
- Setter 注入:使用 setter 方法注入对象的依赖项。
- 接口注入:当一个对象由一个接口定义,并且必须实现该接口才能在运行时注入依赖项时,使用接口注入。因此,设计一个接口来注入依赖项。
DI 的缺点
- 通过实现 DI 而没有真正解耦的猜测。这是关于 DI 最糟糕的部分。
DI 的优点
- 单元测试或可测试的代码
- 可重用的代码
- DI 有助于实现单一职责原则和关注点分离
DI 在以下情况下无效
您将永远不需要
- 不同的实现
- 不同的配置
DI 在以下情况下有效
您需要注入
- 同一依赖项的不同实现
- 同一依赖项到多个组件中
- 配置数据到多个组件中
- 同一实现以不同的配置。
控制反转(IoC)
IoC 指的是容器。它是 DI 用法的一种实现,而 DI 指的是实际的模式。IoC 容器对用于编译时的静态依赖项很有用,而 MEF 对仅在运行时使用的动态依赖项很有用。
.NET IOC 框架列表
- Unity
- 托管可扩展性框架 (MEF)
- Munq
- Funq
- LightCore
- Spring .NET
- Castle Windsor
- Autofac
- Structure Map
- Puzzle.NFactory
- Ninject
- S2Container.NET
- Winter4Net
我们应该使用哪个依赖注入工具
- Munq 适合 Web
- Unity 适合企业
- 如果您想实现整个应用程序,IOC 容器是最好的。
- 如果您只想扩展由第三方开发的部分,那么 MEF 可能是最好的。
- Ninject 是一个太慢的容器。
请注意,存在一些哲学上的矛盾。但我的主要目的是给您一个概念,以便您可以轻松理解。这不是死记硬背的规则,所以如果您有不同的意见,您也是正确的。
历史
- 2017 年 4 月 19 日:初始版本