<
极客APP-每日一课笔记-如何设计低耦合易复用的软件架构
>
上一篇

python快速绘制折线图
下一篇

在春天放弃

**主讲 李智慧 同程艺龙交通首席架构师 **

从框架引出问题

我们经常使用 Spring、Mybatis 各种框架,而 Tomcat、Jetty 这类 web 容器也可以归为框架。这些框架的一个特点就是我们用其开发程序时,无需在程序中调用框架的代码,就可以使用框架的功能特性:我们不需要调用 Spring 的代码就可以使用 spring 的依赖注入、MVC 这些特性开发出低耦合高内聚的代码,我们更不需要调用Tomcat 的代码就可以监听HTTP协议端口、处理HTTP请求

这些框架我们每天都在使用,司空见惯,觉得理所当然

但我们停下来想想,不觉得很神奇吗。我们自己也写代码,我们可以让其他工程师不调用我们写的代码,就可以使用我们代码的功能特性吗

大多数开发者是做不到的

而这些框架又是如何做到的呢,我们先看一下Spring、Tomcat这些框架设计的核心关键点,也是面向对象设计的核心原则之一—依赖倒置原则

依赖倒置原则

  1. 高层模块不应该依赖低层模块,二者都应该依赖抽象
  2. 抽象不应该依赖具体实现,具体实现应该依赖抽象

软件分层设计早就是共识,最早引入软件分层设计正是为了建立清晰的软件分层关系,便于高层模块依赖低层模块

一般的应用程序中,策略层会依赖方法层,业务逻辑层会依赖数据存储层,我们通常都是这样设计的。但这种高层模块依赖低层有什么缺点呢

(1)维护困难,高层模块通常是业务逻辑和策略模型,是一个软件的核心所在,正是高层模块使一个软件区别与其它软件,而低层模块更多的是技术细节,如果高层模块依赖底层模块,就是业务逻辑依赖技术细节,技术细节的改变将会影响业务逻辑,致使业务逻辑也不得不做出改变,因为技术细节的改变而影响代码的改变,这是不合理的

(2)复用困难,通常高层模块复用的价值更高(我理解这是从业务开发的角度来说),高层模块的复用导致了低层模块的连带复用,使复用变得困难

事实上,我们在软件开发中很多地方都使用了依赖倒置原则,我们在 Java 开发中访问数据库,我们的代码并不直接依赖数据库的驱动,而是依赖 JDBC,各种数据库的驱动都实现了 JDBC 规范,当我们的应用程序更换数据库的时候,不需要修改任何代码,这正是我们的业务模块(高层模块)不依赖数据库驱动(低层模块),而是依赖 JDBC (依赖抽象),而数据库驱动作为低层模块也依赖 JDBC,实现 JDBC;同样的 web 开发中也不需要依赖 Tomcat 这样的容器,只要依赖 J2EE 的规范,实现 J2EE 的 Servlet 接口,然后把应用程序打包,通过 web 容器启动就可以处理 HTTP 请求了,这个 web 容器可以是 Tomcat、Jetty、 JBOSS 任何实现 J2EE 规范的容器,这里高层模块也没有依赖低层模块,大家都依赖 J2EE 规范。其它我们熟悉的 MVC 框架、ORM 框架都是遵循了依赖倒置原则

依赖倒置原则设计原理

我们习惯上的层次依赖示例:策略层依赖方法层,方法层依赖工具层,存在的潜在问题是策略层对方法层和工具层是传递依赖的,下面两层的任何改动都会引起这种传递依赖导致的级联改动,这在软件维护过程中是非常糟糕的

解决方法,就是利用依赖倒置的设计原则,每个高层模块都为它所需要的服务声明一个抽象接口,而低层模块则实现这些抽象接口,高层模块通过抽象接口使用低层模块,这样高层模块就不需要直接依赖低层模块。而是低层模块依赖高层模块定义的抽象接口,从而实现依赖倒置,解决了策略层、方法层、工具层的传递依赖问题

我们通常的开发,也是依赖抽象接口,而不是依赖具体实现 ,比如web开发中service层依赖DAO层,并不是直接依赖DAO的具体实现,而是依赖DAO提供的抽象接口,那么这种依赖是否是依赖倒置呢

其实并不是,依赖倒置的原则中除了具体实现要依赖抽象,最重要的是抽象是属于谁的抽象。通常的编程习惯中,低层模块拥有自己的接口,高层模块依赖低层模块提供的接口,比如方法层有自己的接口,策略层依赖方法层提供的接口。DAO层定义自己的接口,Service层依赖DAO层定义的接口,但是按照依赖倒置的原则,接口的所有权是被倒置的,也就是说接口被高层模块定义,高层模块拥有接口、低层模块实现接口, 不是高层模块依赖低层模块的接口,而是低层模块依赖高层模块的接口,从而实现依赖关系的倒置

依赖倒置原则适用场景

倒置依赖原则适用于一个类向另一个类发送消息的场景

举例:Button 按钮控制 Lamp 灯泡, 按钮按下的时候灯泡点亮或者关闭

按照常规的设计思路,我们可能会设计出这样的类图关系:Button 类直接依赖 Lamp 类, 这样的问题在于 Button 依赖 Lamp,对于 Lamp 的任何改动,都可能使 Button 受到牵连做出联动的改变。同时,我们也无法复用 Button 类,比如我们期望通过 Button 控制一个电机的启动或停止,这种设计显然难以重用 Button,因为我们的 Button 还依赖着 Lamp

解决之道,就是将 Button 这个类的设计从依赖于实现,重构为依赖于抽象,这里的抽象就是打开或关闭对象,至于具体如何实现开关、开关目标对象是什么都不重要。由 Button 定义一个抽象接口 ButtonServer,在这个接口里描述抽象,打开关闭对象由具体的 Lamp 去实现,从而实现 Button 控制 Lamp 这一功能需求

通过这样依赖倒置的设计,Button 不再依赖 Lamp,而是 Button 依赖抽象 ButtonServer,而 Lamp 也依赖 ButtonServer,高层模块和低层模块都依赖抽象,Lamp 的改动不会影响 Button,而 Button 可以复用控制其它目标对象,比如电机这些有按钮的设备,只要这些设备实现 ButtonServer 接口就可以了。这里再次强调一下,抽象接口 ButterServer 的所有权是倒置的,它不属于低层模块 Lamp,而属于高层模块 Button,我们从命名也可以看出来,这就是依赖倒置原则的精髓所在

回答开头提到的问题,如何使其他工程师不调用我们的代码,就能够使用我们代码的功能特性呢

如果我们是 Button 的开发者,那么只要其他工程师实现了我们的定义的 ButtonServer 接口,我们的 Button 就可以调用他们开发的 Lamp 或其它设备,使他们开发的设备代码具有了按钮功能,设备代码开发者不需要调用 Button 的代码就拥有了 Button 的功能,而我们 Button 的开发者也不需要关心 Button 会在什么样的的设备代码中使用,所有实现 ButtonServer 的设备都可以使用 Button 的功能,所以依赖倒置原则也被成为好莱坞原则:

Don’t call me, I will call you

不要来调用我,我会调用你。 Tomcat、Spring 都是基于这一原则开发出来的,应用程序不需要调用 Tomcat 或 Spring 框架,而是框架调用应用程序,而实现这一特性的前提就是应用程序必须实现框架的接口规范

依赖倒置编码建议

遵循依赖倒置原则进行编码时,有几点守则:

依赖倒置最典型的使用场景就是框架的设计,框架提供核心功能(例如HTTP处理、MVC等) ,并提供了一组接口规范,应用程序只需要遵循接口规范编码,就可以被框架调用。程序使用框架的功能,但是不调用框架的代码,而是实现框架的接口,被框架调用,从而框架有更高的可复用性,被应用与各种软件开发中

软件代码同样可以参考框架这种模式,开发出低耦合、易复用的软件代码

Top
Foot