后端面试38讲_14_12_软件设计的依赖倒置原则如何不依赖代码却可以复用它的功能
你好,我是李智慧。
在软件开发的过程中呢,我们经常会使用各种编程框架。
如果你使用是java,那么你可能会比较熟悉spring啊,my betitest等等。
事实上呢tocatat解腿这类web容器也可以归类为框架。
框架的一个特点是当开发者使用框架开发一个应用程序时,不需要在程序中调用框架的代码,就可以使用框架的功能特性。
比如说啊程序不需要调用spring的代码,就可以使用spring依赖注入MVC.这些特性开发出低偶和高耐聚的应用代码。
我们的程序更不需要调用tomcat at代码,就可以监听HTP协议,端口处理HTTP协议请求。
这些框架呢我们每天都在使用,已经司空见惯,所以觉得这种实现理所当然。
但是我们停下来好好想一想,难道不觉得这很神奇吗?我们自己也写代码,能够做到让其他工程师不调用我们的代码,就可以使用我们代码的功能特性吗?就我观察大多数开发者是做不到的。
那么spring tomcat这些框架是如何做到的呢?我们看一下spring tomcat这些框架设计的核心关键点,也就是面向对象的基本设计原则之一,依赖倒置原则。
依赖倒置原则是这样的,高层模块不应该依赖低层模块,二者都应该依赖抽象。
抽象不应该依赖具体实现,具体实现应该依赖抽象。
软件分层设计已经是软件开发者的共识。
事实上啊,最早引入软件分层设计,正是为了建立清晰的软件分层关系,便于高层模块依赖低层模块。
一般的应用程序中策略层会依赖方法层,业务逻辑层会依赖数据重储层。
这正实我们日常软件设计开发的常规方式。
那么这种高层模块依赖低层模块的分层依赖方式有什么缺点呢?一是维护困难。
高层模块通常是业务逻辑和策略模型,是一个软件的核心所在。
这是高层模块,是一个软件,取别于其他软件,而低层模块更多的是技术细节。
如果高层模块依赖低层模块,那么就是业务逻辑依赖技术细节。
技术细节的改变将会影响到业务逻辑,业务逻辑也不得不做出改变。
因为技术细节的改变而影响业务代码的改变,这是不合理的。
二是复用困难,通常越是高层模块复用的价值越高。
但是如果高层模块依赖低层模块,那么对于高层模块的依赖呢,将会导致对低层模块的连带依赖,使复用变得困难。
事实上呢在我们软件开发中,很多地方都使用了依赖倒置原则。
我们在java开发中访问数据库代码并不直接依赖数据库的驱动,而是依赖GDBC各种数据库的启动都实现了GDBC.当应用程序需要更换数据库的时候,不需要修改任何代码。
这正是因为应用代码高层模块不依赖数据库驱动,而是依赖抽象GDBC.而数据库驱动作为低层模块,也依赖GDBC.同样的,java开发的web应用也不需要依赖tomcat这样的web容器,只需要依赖g to e规范web应用,实现GTE规范的的lelight接口,然然后都依程程序打包,通过web容器启动就可以处理HTTP请求了。
这个web容器可以是tomcat,也可以是阶听任何实现了g to e规范的web容器都可以。
同样在这里,高层模块不依赖低层模块,大家都依赖g to EE规范。
其他我们所熟悉的MVC框架,ORM框架也都遵循依赖倒置原则。
下面我们进一步了解一下依赖倒置原则的设计原理。
看看如何在我们的程序设计中也能够利用依赖倒置原则,开发出更少依赖、更低耦合、更可复用的代码。
这们习惯上的层次依赖是策略层、依赖方法层、方法层、依赖工具层。
我们分层依赖的一个潜在问题是策略层对方法层和工具层是传递依赖的。
下面两层的任何改动都会导致策略层的改动。
这种传递依赖导致的节点改动可能会导致软件维护过程非常糟糕。
解决办法是利用依赖倒置的设计原则,每个高层模块都为它所需要的服务声明一个抽象接口,而低层模块呢则实现这些抽象接口。
高层模块通过抽象接口使用低层模块,这样高层模块就不需要直接依赖低层模块,而变成了低层模块。
依赖高层模块定义的抽象接口,从而实现了依赖倒置,解决了策略层、方法层、工具层的传递依赖问题。
我们日常的开发通常也要依赖抽象接口,而不是依赖具体实现。
比如web开发中、设备层,依赖DIO层并不是直接依赖DIO的具体实现,而是依赖DLO提供的抽象接口。
那么这种依赖是否是依赖倒置呢?其实并不是依赖倒置。
原则中除了具体实现要依赖抽象,最重要的是抽象是属于谁的。
抽象通常的编程习惯,中低层模块拥有自己的接口,高层模块依赖低层模块提供的接口,比如方法层有自己的接口,策略层依赖方法层的接口,DL层定赖自己的接口,设位实层依赖DIO层定义的接口。
但是按照依赖倒置原则,接口的所有权是被倒置的。
也就是说啊,接口被高层模块定义,高层模块拥有接口,低层模块实现接口不是高层模块依赖低层模块的接口,而低层模块依赖高层模块的接口,从而实现依赖关系的倒置。
在上面的依赖层次中,每一层的接口都被高层模块定义,由低层模块实现,高层模块完全不依赖低层模块。
即使是低层模块的接口,这样的话呢低层模块的改动不会影响高层模块,高层模块的复用,也不会依赖低层模块。
对于service和DL这个例子来说,就是service定义接口,DIO实现接口,这样才符合依赖倒置原则。
依赖倒置原则适用于一个类向另一个类发送消息的场景。
我们再看一个例子,button按钮控制lamp灯泡按钮,按下的时候灯泡点亮或者关闭。
按照常规的设计思路,我们可能会设计出buton类直接依赖lamp. Play这样设计的,问题在于button依赖lamp,那么对lamp的任何改动都可能会使button受到牵连做出联动的改变。
同时我们也无法重用button类。
比如我们希望通过button控制一个电机的启动或者停止这种设计,显义难以重用。
Button由我们button还依赖着lamp呢,解决之道就是将这个设计中的依赖于实现,重构为依赖于抽象。
这里的抽象就是打开关闭目标对象,至于具体的实现细节,比如开关指令如何产生,目标对象是什么都不重要。
这是重构后的设计,由button定义一个抽像接口button server由button server中描述抽象,打开关闭目标对象,由具体的目标对象,比如lamp实现这个接口,从而完成button控制lamp这一功能需求。
通过这样一种依赖倒置,button不再依赖lamp,而是依赖抽象button server,而lamp呢也依赖button server.高层模块和低层模块都依赖抽象lamp的改动不会再影响button.而button可以附用控制其他目标对象,比如电机或者任何有按钮控制的设备,只要这些设备实现button server接口就可以了。
这里再强调一次抽象接口button. Server的所有权是倒置的,它不属于底层模块lamp,而是属于高层模块button.我们从命名上就能够看得出来,这正是依赖倒置原则的精髓所在。
这也正好回答了开头提出的问题,如何使其他工程师不调用我们的代码,就能够使用我们代码的功能特性。
如果我们是button的开发者,那么只要其他工程师的代码实现了我们定义的button server接口button就可以调用他们开发的lamper,或者任何其他有按钮控制的设备,使设备代码拥有了按钮功能。
设备的代码开发者不需要调用button的代码,就拥有了button的功能。
而我们也不需要关心button会在什么样的设备代码中使用所有实现button server的设备,都可以使用button功能,所以依赖倒置原则也被称为好莱坞原则。
Don't call me, i will call you.既不要来调用我,我会调用你。
Tomcat. Spring都是基于这一原则设计出来的,应用程序不需要调用tomcat或者spring这样的框架,而是由框架调用应用程序。
而实现这一特性的前提就是应用程序必须实现框架的接口规范。
比如实现so的接口,依赖倒置原则,通俗的说就是高层模块,不依赖低层模块,而是都依赖抽象接口。
这个抽象接口通常是由高层模块定义,低层模块实现遵循。
一类倒置原则有这样几个编码守则,一、应用代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
二、不要继承具体类。
如果一个类在设计之初不是抽象类,那么尽量不要去继承它。
对类的继承是一种强依赖关系,维护的时候难以改变。
三、不要重写,包含具体实现的函数依赖倒置原则。
最典型的使用场景呢就是框架的设计框架,提供框架核心功能,比如HTTP处理、MAC等,并提供一组接口规范应用程序,只需要遵循接口规范编程,就可以被框架调用程序使用框架的功能。
但是,不调用框架的代码,而是实现框架的接口被框架调用应用框架有更高的可复用性,被应用于各种软件开发中。
我们的代码也可以参照依赖倒置原则,参考框架的设计理念,开发出灵活利偶和可复用的软件代码。
软件开发有时候像变模术一样,常常表现出违反常识的特性,让人目眩神云。
而这正是软件编程这门艺术的魅力所在,感受到这种魅力在自己的软件设计开发中体现出这种魅力,你就迈进了软件高手的大门。
除了文中的例子,还有哪些软件设计遵循了依赖倒置原则。
这些软件中低层模块和高层模块共同依赖的抽象是什么?欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友,或者同事一起交流一下。