依赖项反转原理是什么,为什么重要?
依赖项反转原理是什么,为什么重要?
Answers:
签出该文档:Dependency Inversion Principle。
它基本上说:
简而言之,为何重要:变更是有风险的,并且通过依赖概念而非实施来减少呼叫站点的变更需求。
有效地,DIP减少了不同代码段之间的耦合。这个想法是,尽管有很多实现日志工具的方法,但是您使用它的方法应该在时间上相对稳定。如果可以提取一个表示日志记录概念的接口,则该接口在时间上应比其实现稳定得多,并且呼叫站点受维护或扩展该日志记录机制时所做的更改的影响应小得多。
通过使实现也依赖于接口,您可以在运行时选择哪种实现更适合您的特定环境。根据情况,这也可能很有趣。
《 C#中的敏捷软件开发,原理,模式和实践》以及《 C#中的敏捷原理,模式和实践》是充分理解依赖反转原理背后的原始目标和动机的最佳资源。“依赖倒置原则”一文也是一个很好的参考资料,但是由于它是草案的精简版,最终被引入到前面提到的书中,因此它省略了一些关于“概念”的重要讨论。包和接口所有权是区分此原理与更一般的建议的关键,建议在“设计模式”一书(Gamma等)中找到“对接口进行编程,而不是对实现进行编程”。
为了提供总之,依赖倒置原则主要是关于反转从“层次”组件“较低级”组件具有依赖性,使得“较低级”组件是依赖于接口的常规方向拥有的“更高级别”的部件。(注意:这里的“更高级别”组件是指需要外部依赖项/服务的组件,而不一定是其在分层体系结构中的概念位置。)这样做时,耦合并没有减少太多,因为它从理论上的组件转移了对理论上更有价值的组件而言,价值较小。
这是通过设计组件来实现的,这些组件的外部依赖性通过接口来表示,组件的使用者必须为其提供实现。换句话说,定义的接口表示组件所需的内容,而不是组件的使用方式(例如,“ IneedSomething”而不是“ IDoSomething”)。
依赖关系反转原理所指的不是通过使用接口(例如MyService→[ILogger→Logger])抽象依赖关系的简单实践。尽管这使组件与依赖项的特定实现细节脱钩,但它不会反转使用者和依赖项之间的关系(例如[MyService→IMyServiceLogger]⇐Logger。
依赖倒置原则的重要性可以归结为一个单一目标,即能够重用依赖于外部依赖的软件组件来实现其功能的一部分(记录,验证等)。
在此重用的总体目标内,我们可以描述重用的两种子类型:
在具有子依赖实现的多个应用程序中使用软件组件(例如,您已经开发了一个DI容器,并希望提供日志记录,但又不想将您的容器与特定的记录器耦合,因此使用该容器的每个人也必须使用您选择的日志记录库)。
在不断发展的上下文中使用软件组件(例如,您已经开发了业务逻辑组件,这些组件在实现细节不断发展的应用程序的多个版本中保持不变)。
第一种情况是在多个应用程序中重用组件,例如使用基础结构库,因此目标是向消费者提供核心基础结构需求,而又不将消费者与自己库的子依赖关系联系在一起,因为对此类依赖关系的依赖要求您消费者也需要相同的依赖项。当您的库的使用者选择使用其他库来满足相同的基础结构需求(例如NLog与log4net)时,或者如果他们选择使用与该版本不向后兼容的所需库的更高版本,这可能会出现问题。您的图书馆要求的。
在重用业务逻辑组件(即“高级组件”)的第二种情况下,目标是将应用程序的核心域实现与实现细节的不断变化的需求(例如,更改/升级持久性库,消息传递库)隔离开,加密策略等)。理想情况下,更改应用程序的实现细节不应破坏封装应用程序业务逻辑的组件。
注意:有些人可能反对将第二种情况描述为实际重用,这是因为在单个不断发展的应用程序中使用的组件(例如业务逻辑组件)仅表示一次使用。但是,这里的想法是,对应用程序实现细节的每次更改都会呈现新的上下文,因此会呈现不同的用例,尽管最终目标可以区分为隔离与可移植性。
尽管在第二种情况下遵循依赖关系反转原理可以带来一些好处,但应注意,它在应用于Java和C#等现代语言中的价值已大大降低,甚至可能变得无关紧要。如前所述,DIP涉及将实现细节完全分离到单独的程序包中。但是,在不断发展的应用程序中,即使实现细节最终驻留在同一个包中,由于实现细节组件的需求不断变化,简单地利用根据业务领域定义的接口也可以避免需要修改更高级别的组件。 。原则的这一部分反映了与该语言进行编纂(即C ++)相关的语言方面,这些方面与较新的语言无关。那就是
有关此原理的更长时间的讨论,因为它涉及接口的简单使用,依赖注入和分离的接口模式,可以在这里找到。此外,可以在此处找到有关该原理与动态类型语言(例如JavaScript)的关系的讨论。
在设计软件应用程序时,我们可以考虑实现基本和主要操作的低级类(磁盘访问,网络协议等),以及封装复杂逻辑的高级类(业务流等)。
最后一个依赖于低级别的类。实现此类结构的自然方法是编写底层类,一旦让他们编写复杂的高层类。由于高级类是根据其他类定义的,因此这似乎是合乎逻辑的方法。但这不是一个灵活的设计。如果我们需要替换低级课程,会发生什么?
依赖倒置原则指出:
该原理试图“反转”软件中高级模块应依赖于较低级模块的传统观念。在这里,高级模块拥有由低级模块实现的抽象(例如,确定接口的方法)。因此,使较低级别的模块依赖于较高级别的模块。
良好应用的依赖关系反转可在应用程序整个体系结构级别提供灵活性和稳定性。这将使您的应用程序更安全,稳定地发展。
传统上,分层体系结构UI依赖于业务层,而后者又依赖于数据访问层。
您必须了解层,包或库。让我们看看代码将如何。
我们将为数据访问层提供一个库或包。
// DataAccessLayer.dll
public class ProductDAO {
}
以及另一个依赖于数据访问层的库或包层业务逻辑。
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
依赖项反转指示以下内容:
高级模块不应依赖于低级模块。两者都应依赖抽象。
抽象不应依赖细节。细节应取决于抽象。
什么是高级模块和低级模块?思维模块(例如库或软件包),高级模块将是传统上具有依赖关系且依赖于它们的低级别的模块。
换句话说,模块高级别将是调用动作的地方,模块低级别是执行动作的地方。
从这个原理可以得出一个合理的结论,即构想之间不应有依赖关系,而必须有对抽象的依赖关系。但是根据我们采用的方法,我们可能会误用投资依赖依赖,而只是一种抽象。
想象一下,我们将代码修改如下:
我们将为定义抽象的数据访问层提供一个库或包。
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
以及另一个依赖于数据访问层的库或包层业务逻辑。
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
尽管我们依赖于抽象,但是业务和数据访问之间的依赖关系仍然相同。
为了获得依赖关系反转,必须在此高级逻辑或域所在的模块或程序包中而不是在低级模块中定义持久性接口。
首先定义域层是什么,其通信的抽象定义为持久性。
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
持久层取决于域之后,如果已定义依赖项,则现在开始反转。
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(来源:xurxodev.com)
重要的是要很好地吸收概念,加深目的和好处。如果我们坚持不懈地学习典型的案例库,我们将无法确定在哪里可以应用依赖原理。
但是为什么我们要反转依赖关系呢?除了特定示例,主要目标是什么?
这样通常可以使不依赖于不稳定程度较低的最稳定事物更频繁地更改。
与设计用于与持久性进行通信的域逻辑或操作相比,更改持久性类型(数据库或技术来访问同一数据库)要容易得多。因此,依赖关系被逆转,因为如果发生这种变化,则更容易更改持久性。这样,我们将不必更改域。域层是所有层中最稳定的,这就是为什么它不应该依赖任何东西的原因。
但是,不仅有这个存储库示例。在许多情况下都可以应用此原理,并且有基于此原理的体系结构。
在某些体系结构中,依赖关系反转是其定义的关键。在所有域中,它是最重要的,它是抽象,它指示域和其余包或库之间的通信协议已定义。
在“ 干净的体系结构”中,域位于中心,如果您从指示依赖性的箭头方向看,很明显,最重要和最稳定的层是什么。外层被认为是不稳定的工具,因此请避免依赖它们。
(来源:8thlight.com)
对于六角形架构,它的发生方式相同,该域也位于中央部分,端口是从多米诺骨牌向外通信的抽象。再次显然,该域是最稳定的,并且传统的依赖关系被倒置了。
对我来说,依赖性反转原则,如官方文章中所述确实是一种错误的尝试,它试图提高本质上不可重用的模块的可重用性,并且是解决C ++语言问题的一种方法。
C ++中的问题是头文件通常包含私有字段和方法的声明。因此,如果高级C ++模块包括低级模块的头文件,则将取决于实际实现。该模块的细节。显然,这不是一件好事。但这在当今常用的更现代的语言中不是问题。
高级模块本质上比低级模块具有较低的可重用性,因为前者通常比后者具有更多的应用程序/上下文特定性。例如,实现UI屏幕的组件是最高级别的,并且也非常(完全?)特定于该应用程序。尝试在不同的应用程序中重用这样的组件会适得其反,只会导致过度设计。
因此,只有在组件A对于在不同应用程序或上下文中重用确实有用的情况下,才能在依赖于组件B(不依赖于A)的组件A的同一级别上创建单独的抽象。如果不是这种情况,那么应用DIP将是错误的设计。
陈述依赖倒置原则的更清晰方法是:
封装复杂业务逻辑的模块不应直接依赖于封装业务逻辑的其他模块。相反,它们应仅依赖于简单数据的接口。
即,与其Logic
像通常那样实现您的课程,不如:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
您应该执行以下操作:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
并且DataFromDependency
应与一起位于同一模块中Logic
,而不是Dependency
。
为什么这样
Dependency
修改,你不需要改变Logic
。Logic
操作是一个简单得多的任务:它仅对看起来像ADT的对象起作用。Logic
现在可以更容易地进行测试。您现在可以直接实例化Data
虚假数据并将其传递。无需模拟或复杂的测试支架。DataFromDependency
直接引用Dependency
的与处于同一模块中Logic
,则在编译时Logic
模块仍直接依赖于Dependency
模块。按照Bob叔叔对原理的解释,避免这就是DIP的重点。而是,要遵循DIP,Data
应与放在同一模块中Logic
,但DataFromDependency
应与在同一模块中Dependency
。
控制反转(IoC)是一种设计模式,在这种模式下,对象由外部框架处理其依赖关系,而不是向框架询问其依赖关系。
使用传统查找的伪代码示例:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
使用IoC的类似代码:
class Service {
Database database;
init(database) {
this.database = database;
}
}
IoC的好处是:
依赖性反转的重点是使软件可重用。
这个想法是,它们依赖于一些抽象的接口,而不是两个相互依赖的代码。然后,您可以重用任何一块,而无需另一块。
最常见的实现方法是通过控制反转(IoC)容器(如Java中的Spring)来实现。在此模型中,对象的属性是通过XML配置设置的,而不是通过退出对象并找到它们的依赖关系来设置的。
想象一下这个伪代码...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass直接依赖于Service类和ServiceLocator类。如果要在另一个应用程序中使用它,则需要这两个条件。现在想象一下...
public class MyClass
{
public IService myService;
}
现在,MyClass依赖于单个接口IService接口。我们让IoC容器实际设置该变量的值。
因此,现在,MyClass可以轻松地在其他项目中重用,而无需带来其他两个类的依赖。
更好的是,您不必拖动MyService的依赖关系,这些依赖关系的依赖关系,以及……嗯,您就明白了。
如果我们可以假设一个公司的“高级”员工因执行计划而获得报酬,并且这些计划是由许多“低级”员工计划的总执行来交付的,那么我们可以说如果高级别员工的计划说明以任何方式与任何低级别员工的特定计划相结合,则通常是一个糟糕的计划。
如果高层管理人员有一个“改善交货时间”的计划,并指出船运公司的员工每天早上必须喝咖啡并且伸懒腰,那么该计划是高度耦合的,而且凝聚力很低。但是,如果该计划没有提及任何特定员工,而实际上仅要求“一个可以执行工作的实体准备工作”,那么该计划将变得松散耦合且更具凝聚力:这些计划不会重叠,并且很容易被替换。承包商或机器人可以轻松替换员工,高层的计划保持不变。
依赖倒置原则中的“高级”意味着“更重要”。
我可以看到上面的答案给出了很好的解释。但是我想用简单的例子提供一些简单的解释。
依赖关系反转原理允许程序员删除硬编码的依赖关系,以便使应用程序变得松散耦合和可扩展。
如何实现:通过抽象
没有依赖反转:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
在上面的代码片段中,地址对象是硬编码的。相反,如果我们可以使用依赖关系反转并通过构造函数或setter方法注入地址对象。让我们来看看。
使用依赖关系反转:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
依赖倒置:依赖抽象,而不依赖具体。
控制反转:Main与Abstract,以及Main是系统的粘合剂。
这些是一些谈论这个的好帖子:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
假设我们有两个类:Engineer
和Programmer
:
类工程师依赖于Programmer类,如下所示:
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
在这个例子中,类Engineer
依赖于我们的Programmer
类。如果我需要更改该Programmer
怎么办?
显然,我也需要更改Engineer
。(哇,这时OCP
也违反了)
然后,我们该如何清理这个烂摊子?答案实际上是抽象。通过抽象,我们可以删除这两个类之间的依赖关系。例如,我可以Interface
为Programmer类创建一个,从现在开始,每个想要使用的类Programmer
都必须使用其Interface
,然后通过更改Programmer类,我们不需要更改任何使用它的类,因为我们抽象用过的。
注:DependencyInjection
可以帮助我们做的DIP
和SRP
太。
除了一连串的好答案之外,我还想添加一个自己的小样本,以展示良好与不良做法。是的,我不是丢石头的人!
假设您想要一个小程序通过控制台I / O 将字符串转换为base64格式。这是幼稚的方法:
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
DIP基本上说高级组件不应该依赖于低级实现,根据Robert C. Martin(“清洁架构”),“级”是与I / O的距离。但是您如何摆脱这种困境呢?只需使中央编码器仅依赖于接口,而不必理会它们的实现方式:
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
请注意,您无需触摸GoodEncoder
即可更改I / O模式-该类对自己知道的I / O接口感到满意;任何低级实现IReadable
和IWriteable
永远不会打扰它。
GoodEncoder
您的第二个示例。要创建DIP示例,您需要引入一个概念,该概念是“拥有”您在此处提取的接口-尤其是将其与GoodEncoder放在同一包中,而将其实现保留在外部。
依赖倒置原则(DIP)说
i)高级模块不应依赖于低级模块。两者都应依赖抽象。
ii)抽象不应依赖细节。细节应取决于抽象。
例:
public interface ICustomer
{
string GetCustomerNameById(int id);
}
public class Customer : ICustomer
{
//ctor
public Customer(){}
public string GetCustomerNameById(int id)
{
return "Dummy Customer Name";
}
}
public class CustomerFactory
{
public static ICustomer GetCustomerData()
{
return new Customer();
}
}
public class CustomerBLL
{
ICustomer _customer;
public CustomerBLL()
{
_customer = CustomerFactory.GetCustomerData();
}
public string GetCustomerNameById(int id)
{
return _customer.GetCustomerNameById(id);
}
}
public class Program
{
static void Main()
{
CustomerBLL customerBLL = new CustomerBLL();
int customerId = 25;
string customerName = customerBLL.GetCustomerNameById(customerId);
Console.WriteLine(customerName);
Console.ReadKey();
}
}
注意:类应取决于接口或抽象类之类的抽象,而不取决于特定的细节(接口的实现)。