写点什么

设计原则 — D 依赖反转原则

作者:Lemoon Can
  • 2023-05-12
    浙江
  • 本文字数:2705 字

    阅读完需:约 9 分钟

设计原则 — D 依赖反转原则

含义

依赖反转原则指的是通过抽象接口的方式,可实现源代码依赖方向永远是控制流方向的反转。


对于扩充解释,读者如果第一次看到,仍然会一脸茫然。

要具体解释含义的话,我们先来看看这张图:

通过横线分隔,上方是依赖未反转情况,下方是依赖反转情况。

我们来想象一个这样的场景,Application 需要使用 AServiceImpl 的功能,此时我们定义在控制方向上是 Application 控制 AServiceImpl。

  1. 横线上方的图,AServiceImpl 直接整合进了 Application 中,此时我们定义源代码的依赖方向是 Applicaition 依赖了 AServiceImpl,与控制方向一致。

  2. 而横线下方的图,抽象出了 AService 接口整合进 Application , 不再是 AServiceImpl 整合进 Applicaiton,且 AServiceImpl 实现了 AService,即 AServiceImpl 依赖了 AService,那可以说源代码的依赖方向反转了,与上述源代码依赖方向相反,与控制方向也相反。


对于上面的描述可能会产生两个问题(这是我初始阅读的困惑):

  1. 为什么 AServiceImpl 依赖了 AService 就是源代码依赖方向反转了呢,Application 不还依赖着 AService?

因为 AService 与 Application 属于同一层次,AService 是基于 Application 需要何功能而抽象出来的一个接口,所以 AServiceImpl 依赖了与 Applicaiton 同一层次的 AService,且 AServiceImpl 实现了 AService,反而是 AServiceImpl 依赖了与 Applicaiton 同一层次的 AService,以此来看,依赖方向是不是反转了呢?

最重要需要说明的是,通过抽象接口的方式后,做到了 Application 中无任何 AServiceImpl 的身影。

  1. 通过抽象接口的方式,控制方向不变,这是如何做到的呢?虽然源代码层次中 Applicaiton 完全不依赖 AServiceImpl,但如何做到控制方向上是 Application 控制 AServiceImpl 呢?

答案是可通过多态、控制反转来实现。Application 引入的是 AService 接口,其实例通过控制反转,也就是外部容器创建好后再传入。所以理论上上图还需引入工厂类。

说句题外话,我们经常看到依赖反转、控制反转、依赖注入这三个名词,有些类似,那此三者之间有什么联系吗?

篇幅所限,在这只说我的思考结论,有兴趣的读者也可自己思考下。我的结论是:控制反转是实现依赖反转原则的方式的链路上的一环。依赖注入是实现控制反转的方式。


言归正传,从此图再看原则的解释,其实反转的是源代码依赖方向,与控制方向永远相反


那有读者可能会说了,我们平常看到的原则并不是这样的,多是像下面这样的描述,包含三项内容:

  1. 高层模块不应依赖于低层模块,高层和低层应通过抽象来具体依赖

  2. 抽象不应依赖于具体实现

  3. 具体实现要依赖于抽象


这和前面的解释看起来不一样呀?别急,且听我慢慢道来。

先说结论:此三项内容是上图中依赖未反转变成依赖反转的实施准则的精华总结。

  1. 高层模块不应依赖于低层模块,高层和低层应通过抽象来具体依赖。表示当高层模块需要依赖低层模块时,应通过抽象接口来依赖;对应上图,高层模块是 Application,低层模块是 AServiceImpl,此时高层模块需要依赖低层模块,我们就可抽象出 AService 接口,通过 AService 接口来做依赖。

  2. 抽象不应依赖于具体实现。表示抽象接口应基于高层模块所需要的功能来定义,而不是通过实现来定义,且不应有任何实现的细节。对应上图,AService 接口与 Application 同属一层,而不是与 AServiceImpl 同属一层。

  3. 具体实现要依赖于抽象。表示不要在具体实现类上创建衍生类、不要覆盖包含具体实现的函数,即具体实现要依赖于抽象。此项更多是提出对实现的要求,对应上图,也就是对 AServiceImpl 的要求。


总结来说,解释 1 描述的是依赖反转的现象,解释 2 描述的是实现依赖反转现象的三项重要准则。对应开头的话“通过抽象接口的方式,可实现源代码依赖方向永远是控制流方向的反转”,解释 1 在描述后半句的现象,解释 2 在描述该遵循怎样的准则来抽象接口可以实现依赖反转的现象。

两者结合,会有一个更清晰的认识。


原作者在描述原则时,还提到了原则的核心是:在源代码的依赖层次关系中应多引用抽象类型,而非具体实现。这在上述两个解释中也能略微意会到。


不过此原则确实不好理解,我看了原作者的解释才略知一二,如果读者觉得看不懂也不用气馁,先大致留个印象,隔断时间再翻出来看看。同时也很建议看看原作者的书籍(架构整洁之道)里的描述。

优点

前面解释了一大通,可为什么要这么做呢?我们得来看看它的优点:

我发现有一个超大优点:解耦

以依赖方需求而抽象出的接口相对稳定,那依赖方依赖的是稳定的接口,实现方依赖的也是稳定的接口,那依赖方所属层次就是稳定的,不会受实现方的影响,因为实现方是可以任意替换的。这是不是就意味着依赖方与被依赖方解耦了?


仔细观察,发现生活中有许多类似例子,比如供电方、电器两者之间引入了插座,定好插座的标准,由电器适配插座的标准,此后供电方只要适配插座,电器也只要适配插座,供电方不需要了解会有哪些电器。


接口就类似于插座,定义了标准,可实现依赖方与被依赖方的解耦。

解耦对应可以达到好代码的衡量标准中的多项要求:灵活性、可维护性、可扩展性。

如何做

那怎么做呢?

其实答案已经在前面原则的解释 2 提到了。只需要再细化一下:

  1. 高层模块依赖低层模块时可以考虑抽象接口来进行依赖

只是目前我们针对高层和低层模块的区分仍是迷茫的。前面是我直接定义了高层模块是 Applicaiton,低层模块是 AServiceImpl,但平常我们该如何分辨呢?

  1. 区分方式 1:“按照”输入与输出之间的距离“来定义的。距“输入与输出”越远层次越高,越近层次越低。输入可以理解为请求,先处理请求的类,离请求越近;后处理请求的类,离请求越远。那高层离请求越远,低层离请求越近。

  2. 区分方式 2:抽象算高层模块,实现算低层模块。

这是两个粗略的区分方式,真正该如何辨别也需多积累经验,日常开发中还是需要多思考。

  1. 考虑低层模块的实现是否稳定来抉择是否要抽象接口

低层模块的实现稳定时无必要非得按照原则来实现,比如 Java 中的 String 类;

低层模块的实现不稳定时,则应抽象出接口,比如 Tomcat、应用程序,由于应用程序多变,抽象出了 Servlet。

  1. 原作者也说了,此原则不是金科玉律,必然存在违反的情况。但存在违反并不意味着可以肆意违反,应将其局限在小部分的实现中,比如 Main 组件。

对比

说到这里,会发现 基于接口而非实现编程依赖反转 原则都提到了接口,那这两者之间有什么联系呢?

在最后,我来总结下两者之间的关系:依赖反转基于接口而非实现编程的补充。

  1. 基于接口而非实现编程告诉我们要多考虑抽象接口。

  2. 依赖反转则告诉我们什么时机可以抽象接口:即当存在依赖关系时,若被依赖方的实现不稳定;那一定要抽象接口,且以依赖方的需求来抽象接口。


《设计模式之美》—— 王争(极客时间)

《架构整洁之道》—— Robert C. Martin

发布于: 刚刚阅读数: 8
用户头像

Lemoon Can

关注

装满月亮的柠檬罐子🌙🌟 2019-02-13 加入

“快乐🤣”的 什么都不精😤的 程序媛👾

评论

发布
暂无评论
设计原则 — D 依赖反转原则_依赖反转原则_Lemoon Can_InfoQ写作社区