依赖倒转原则
♦依赖倒转原则的来由:简单地说,传统的过程性系统的设计办法倾向于使高层次的模块依赖于低层次的模块,即抽象层次依赖于具体层次。倒转原则是要把这个错误的依赖关系倒转过来,即具体层次依赖于抽象层次。
♦依赖(或者耦合)关系的种类:
- 零耦合(Nil Coupling)关系:如果两个类没有耦合关系,就称之为零耦合。
- 具体耦合(Concrete Coupling)关系:具体性耦合发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
- 抽象耦合(Abstract Coupling)关系:抽象耦合关系发生在一个具体类和一个抽象类(或者Java/C#接口)之间,使两个必须发生关系的类之间存有最大的灵活性。以抽象方式耦合是依赖倒转原则的关键。
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
♦依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
变量的静态类型和实际类型:
- 变量被声明时的类型叫做变量的静态类型。
- 变量所引用的对象的真实类型叫做变量的实际类型。
List employees = new Vector();
- 如上代码,employees变量的静态类型是List,而实际类型是Vector。
- 则尽量不要使用声明语句: Vector employees = new Vector();不要使用一个具体类作为变量的类型,要用抽象类型作为类型,程序能具有更好的灵活性。
- 只要一个被引用的对象存在抽象类型,就应当在任何引用此对象的地方使用抽象类型,包括参量的类型声明、方法返还类型的声明、属性变量的类型声明等。
♦在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中。
♦依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。
♦常用的注入方式有三种,分别是:
- 构造注入:是指通过构造函数来传入具体类的对象。
- 设值注入(Setter注入):是指通过Setter方法来传入具体类的对象。
- 接口注入:是指通过在接口中声明的业务方法来传入具体类的对象。
这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
♦举例说明1:
Sunny软件公司开发人员在开发某CRM系统时发现:该系统经常需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,初始设计方案结构如图所示:
在编码实现所示结构时,Sunny软件公司开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时,需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。
在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。我们可以通过引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。更换具体数据转换类时无须修改源代码,只需要修改配置文件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,原有代码无须做任何修改,满足开闭原则。重构后的结构如图所示:
♦在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
♦举例说明2:
public class Email { public void SendEmail() { ... ... } } public class Notification { private Email _email; public Notification() { _email = new Email(); } public void PromotionalNotification() { _email.SendEmail(); } }
上述代码中,Notification类依赖Email类,这违反了依赖倒转原则,而且当我们要增加发送短信/保存到数据库的功能的时候,我们还要改变Notification类。
以下使用抽象类/接口来解耦。
public interface IMessageService { void SendMessage(); } public class Email : IMessageService { public void SendMessage() { ... ... } } public class Notification { private IMessageService _iMessageService; public Notification() { _iMessageService = new Email(); } public void PromotionalNotification() { _iMessageService.SendMessage(); } }
IMessageService是一个接口,而Notification 类只要调用接口的方法/属性就可以了
♦以下用三种注入方式进行演示:
- 构造器注入,最普遍的方式,当一个类需要另一个类的依赖的时候,通过构造函数来提供。
-
public class Notification { private IMessageService _iMessageService; public Notification(IMessageService _messageService) { this._iMessageService = _messageService; } public void PromotionalNotification() { _iMessageService.SendMessage(); } }
- 构造函数实现很简单,Notification类需要知道的很少,想要创建Notification实例的时候看构造函数就可以知道需要什么信息了,因此实现了松耦合。
-
-
设值注入(Setter注入),当依赖可有可无的时候很有用,通过暴露一个可写的属性,允许客户提供不同的依赖实现。
-
public class Notification { public IMessageService MessageService{get; set;} public void PromotionalNotification() { if (MessageService == null) { // some error message } else { MessageService.SendMessage(); } } }
- 没有了构造函数,而用属性来替换,在PromotionalNotifications 方法里需要检查MessageService的值或者提供相应的服务。
-
- 接口注入,当依赖可以对于每个方法调用都不同的时候,我们可以通过一个方法参数来实现,比如我们的这个类还可以发送短信。
-
public class Email : IMessageService { public void SendMessage() { ... ... } } public class SMS : IMessageService { public void SendMessage() { ... ... } } public class Notification { public void PromotionalNotification(IMessageService _messageService) { _messageService.SendMessage(); } }
- IMessageService 接口在两个类中都实现了。我们可以提供不同的类对象作为参数,这样可以有不同的调用效果。
-
【参考:《Java与模式》】
【参考:https://blog.csdn.net/LoveLion/article/details/7562783】
【参考:https://www.cnblogs.com/lazycoding/archive/2012/11/21/2781384.html】