热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

设计模式原则3依赖倒置原则

个人博客:打开链接依赖倒置原则定义依赖倒置原则(DependenceInversionPrinciple,DIP)定义如下:Highlevelmo

个人博客:打开链接

依赖倒置原则定义

依赖倒置原则(Dependence Inversion Principle ,DIP)定义如下:

High level modules should not depend upon low level modules,Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstracts.

翻译过来为:

  • 高层模块不应该依赖低层模块,两者都应该依赖抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

也可以说高层模块,低层模块,细节都应该依赖抽象

每一个逻辑的实现都是由颗粒原子逻辑组成的,颗粒原子逻辑就是低层模块,而颗粒原子逻辑组成的模块就是高层模块。在java语言中,抽象就是接口或抽象类,两都都是不能直接被实例化的,细节就是实现类,实现接口或继承抽象类而产生的类就是细节,两者都可以直接被实例化。

依赖倒置原则在java语言中,表现是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
  • 接口或抽象类不依赖实现类
  • 实现类依赖接口或抽象类

更加精简的定义就是“面向接口编程”—OOD(Object-Oriented Design,面向对象设计)的精髓之一。

依赖倒置原则的好处


  • 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定
  • 降低并行开发引起的风险,提高代码的可读性和可维护性

为什么我们要符合依赖倒置原则

举个例子司机驾驶奔驰车,如图所示类图:
这里写图片描述
司机源代码:

public class Driver {//司机的主要职责就是驾驶汽车public void drive(Benz benz) {benz.run();}
}

司机通过调用奔驰车的run方法开动奔驰车,奔驰车源代码:

public class Benz {//汽车肯定会行驶public void run(){System.out.println("奔驰汽车开始运行...");}
}

有车,有司机,在Client场景类产生相应的对象。其源代码为:

public class Client {public static void main(String[] args){Driver zhangSan = new Driver();Benz benz = new Benz();//张三开奔驰车zhangSan.drive(benz);}
}

以上代码完成了基本的需求,对于软件项目设计“变更才显真功夫”,那么问题来了,来了一辆宝马车,可是张三却没法驾驶,这和现实完全不符。司机可以开任何品牌的车嘛。
宝马车源代码:

public class BMW{//BMW also can be drivenpublic void run(){System.out.println("宝马车开始运行...");}
}

为了让张三可以开任何牌子的车,我们引入依赖倒置原则。引入依赖倒置原则后的类图:
这里写图片描述
司机接口:

public interface IDriver {//是司机就应该会驾驶汽车public void drive(ICar car);
}

司机类的实现:

public class Driver implements IDriver {//司机的主要职责就是驾驶汽车public void drive(ICar car){car.run();}
}

在IDriver中,通过传入ICar接口实现了抽象之间的依赖关系,Driver实现类也传入了ICar接口,至于到底是哪种型号的Car,需要在高层模块中声明。ICar及其两个实现类为:

public interface ICar {//是汽车就应该能跑public void run();
}
public class Benz implements ICar {public void run(){System.out.println("奔驰车开始运行...");}
}
public class BMW implements ICar{public void run(){System.out.println("宝马车开始运行...");}
}

如此这般,我们实现了抽象不依赖于细节,而依赖于抽象,即ICar接口不依赖于BMW或者Benz,而仅仅依赖于ICar,这也是该项目中的高层次模块,业务场景中是这样的:

public class Client {public static void main(String[] args) {IDriver zhangSan = new Driver();ICar benz = new Benz();//张三开奔驰车zhangSan.drive(benz);}
}

这里司机和汽车使用的都是抽象接口,这里的zhangsan以及benz都是按照接口进行操作的,所以屏蔽了细节对接口的影响,进一步,如果有新的需求,zhangsan想要开BMW车,只需要将ICar benz = new Benz()更换为ICar bmw = new BMW()即可,这些都是在业务场景中进行的修改,我们根本不用改动低层次模块Driver,Benz或者是BMW,更不会影响高层模块IDriver或者ICar,这样把风险降到了最小。

我们再来考虑并行开发的影响,之所以会影响并行开发是因为模块依赖,我们知道,模块之间如果有依赖关系,只需要制定出两者之间的接口或者抽象类就可以了,我们继续原来的例子,甲负责IDriver的开发,乙负责ICar开发,只需要提前商定好接口,两者即可独立的进行开发,如果甲开发进度较快,而乙进度滞后,也不会对甲造成什么影响,如果此时甲想要进行单元测试,只需要Mock出一个ICar的实现类对象,即可对IDriver以及实现类Driver进行测试了:

public class DriverTest extends TestCase{Mockery context = new JUnit4Mockery();@Testpublic void testDriver() {//根据接口虚拟一个对象final ICar car = context.mock(ICar.class);IDriver driver = new Driver();//内部类context.checking(new Expectations(){{oneOf (car).run();}});driver.drive(car);}
}

可见,即使两者的开发进度不一样,也可以分别进行测试,这也是测试驱动开发的精髓所在。

抽象,对实现而言,是一个约束,所有实现类必须对抽象进行实现,对其他依赖模块而言,是一种契约,抽象定义的接口,既约束了自己,也约束了自己与外部的关系,保证实现细节不会脱离契约的范畴,为其他依赖模块提供支持,双方按照约定的接口共同发展,只要接口在,细节就不会偏离太远,始终为其他依赖模块提供坚实的依赖细节。

依赖的传递

针对于对象依赖的声明,有三种方式:

通过构造函数传递依赖关系

这种方式通过构造函数声明依赖对象,这种方式叫做构造函数注入,示例如下:

public interface IDriver {//是司机就应该会驾驶汽车public void drive();
}public class Driver implements IDriver{private ICar car;//构造函数注入public Driver(ICar _car){this.car = _car;}//司机的主要职责就是驾驶汽车public void drive(){this.car.run();}
}

通过Setter方法传递依赖关系

这种方式,通过Setter方法,为外部提供设置依赖对象的方式,这种方法叫做Setter依赖注入,示例如下:

public interface IDriver {//车辆型号public void setCar(ICar car);//是司机就应该会驾驶汽车public void drive();
}public class Driver implements IDriver{private ICar car;public void setCar(ICar car){this.car = car;}//司机的主要职责就是驾驶汽车public void drive(){this.car.run();}
}

通过接口方法传递依赖关系

我们最开始使用的,就是这种方法,也叫做接口注入。

最佳实践

依赖倒置原则,在项目中使用可以使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,那我们在项目中怎么应用这个原则呢?有一下几个规则:

  • 尽量有接口或者抽象类


这也是依赖倒置原则的基础,要依赖抽象就必须先有抽象才对。

  • 变量的表面类型,尽量是接口或者抽象类型


并不要求一定是接口或者抽象类型,某些工具类,或者是类的clone方法的使用,就要求是实现类的类型。

  • 实现类尽量不再派生


这只是一个软性规则,在某些场景下,例如设计有缺陷,或者进行项目维护的时候,也是可以从实现类中派生的,根据具体的场景来做措施。

  • 尽量不重写基类的方法


如果基类是抽象类并且已经实现了方法,那么就尽量不要重写该方法,因为该方法有可能会被其他的抽象所依赖,重写该方法,会破坏抽象接口的稳定性。

  • 结合里氏替换原则


里氏替换原则同样可以应用到这里,父类出现的地方子类就可以出现,到我们这里可以理解为基类,接口或者抽象类可以出现的地方,就可以使用实现类对象,由接口来定义公用的属性和方法,抽象类实现公共的构造部分,由实现类实现准确的业务逻辑,并对父类进行细化。

小结

要想彻底理解依赖倒置,我们先来说说依赖正置,依赖正置指的是实现类依赖实现类,这也是我们生活中的思考方式,例如,开车依赖宝马,喝酒依赖二锅头,等等,这些是实现类之间的依赖,而我们编程,需要对现实世界的事物进行抽象,接口和抽象类就是抽象的结果,然后根据我们的系统设计,这些抽象类和接口之间产生了依赖关系,就产生了依赖倒置。

在小型项目中,依赖倒置原则的优点很难体现,但是在大型项目中,依赖倒置原则的优点就会体现出来,特别是用于规避一些非技术原因引起的问题,项目越大,需求变化的概率越大,使用依赖倒置原则对实现类进行约束,可以很好的避免因为需求变化导致工作量剧增,另外,如果在大型项目中人员变动,则使用依赖倒置原则可以避免受到影响,而且维护人员也可以简单轻松的进行维护。

依赖倒置原则是实现开闭原则的基础,只要抓住“面向接口编程”这一核心思想,基本上不会脱离依赖倒置原则太远。另外在项目中,使用依赖倒置原则也要审时度势,每一个设计原则的优点也是有限度的,不要抓住原则不放,而不考虑项目的实际情况,原则是死的,但是设计者是活的,项目的实际状况是动态变化的,项目的最终目标是上线和盈利,技术只是实现目的的工具,切忌耍花枪式的过度设计。


推荐阅读
  • 本文详细介绍了Akka中的BackoffSupervisor机制,探讨其在处理持久化失败和Actor重启时的应用。通过具体示例,展示了如何配置和使用BackoffSupervisor以实现更细粒度的异常处理。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍了Java中org.eclipse.ui.forms.widgets.ExpandableComposite类的addExpansionListener()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。这些示例来源于多个知名开源项目,具有很高的参考价值。 ... [详细]
  • 在前两篇文章中,我们探讨了 ControllerDescriptor 和 ActionDescriptor 这两个描述对象,分别对应控制器和操作方法。本文将基于 MVC3 源码进一步分析 ParameterDescriptor,即用于描述 Action 方法参数的对象,并详细介绍其工作原理。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • Android 渐变圆环加载控件实现
    本文介绍了如何在 Android 中创建一个自定义的渐变圆环加载控件,该控件已在多个知名应用中使用。我们将详细探讨其工作原理和实现方法。 ... [详细]
  • MQTT技术周报:硬件连接与协议解析
    本周开发笔记重点介绍了在新项目中使用MQTT协议进行硬件连接的技术细节,涵盖其特性、原理及实现步骤。 ... [详细]
  • 本文详细介绍 Go+ 编程语言中的上下文处理机制,涵盖其基本概念、关键方法及应用场景。Go+ 是一门结合了 Go 的高效工程开发特性和 Python 数据科学功能的编程语言。 ... [详细]
  • 技术分享:从动态网站提取站点密钥的解决方案
    本文探讨了如何从动态网站中提取站点密钥,特别是针对验证码(reCAPTCHA)的处理方法。通过结合Selenium和requests库,提供了详细的代码示例和优化建议。 ... [详细]
  • 深入理解Cookie与Session会话管理
    本文详细介绍了如何通过HTTP响应和请求处理浏览器的Cookie信息,以及如何创建、设置和管理Cookie。同时探讨了会话跟踪技术中的Session机制,解释其原理及应用场景。 ... [详细]
  • 深入解析Spring Cloud Ribbon负载均衡机制
    本文详细介绍了Spring Cloud中的Ribbon组件如何实现服务调用的负载均衡。通过分析其工作原理、源码结构及配置方式,帮助读者理解Ribbon在分布式系统中的重要作用。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
author-avatar
只喝大瓶的雪碧
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有