软件系统解耦:理解依赖关系(一)

在实际工作中,复杂度上来后,各模块之间错综复杂,调用关系网千头万绪。即使有各种设计模式做指导,做出合理的设计也并不容易。程序员天天疲于应对层出不穷的变化,在不断紧逼的deadline压力下,面对巨大的重构工作量往往感到心有余而力不足。
系统复杂度的根源除了业务本身的复杂度,就是设计了不恰当的耦合关系。本文试图探讨依赖关系的主要类型,并总结应对依赖的编程范式。

耦合:依赖和变化

耦合是一个有歧义的词语(为什么“耦合”概念该要摒弃)。当我们说“A和B耦合”的时候,我们是想表达A和B之间有紧密的联系。具体是什么,不容易讲清楚。
在我看来,耦合至少包含了两个方面的含义:依赖和变化。
业务逻辑固有的复杂度决定了,模块之间必然存在着依赖。规范模块间的依赖关系,就是梳理业务复杂度的过程。最终的成果反映在代码中,代表了对业务复杂度的一种认识。这种认识随着业务需求的变化而演化,随着设计者的能力提升而深化。依赖不能被消除,但是可以被优化。探讨一些应对的范式有助于规避已知的陷阱。
变化则来源于两个方面:发展中的用户需求,完善中的系统模型。用户的需求是我们努力的方向。系统模型则代表了我们对需求的理解,是经验和智慧的结晶。一个完善的系统模型,表达能力要足够强,对业务的适应能力要足够强。变化,意味着工作量,意味着成本,应该尽量降低。如果我们把“系统变更”和“业务需求变更”写成函数:
SystemChange = f(ReqirementChange)
我们希望自变量不变的情况下,“系统变更”这个函数值越小越好。特别是“业务需求变更”在当前系统设计假设条件下产生调整的时候,“系统变更”应该局限在很小的范围内。

依赖的种类

在UML类图中,依赖关系被标记为<>。A依赖B意味着,A模块可以调用B模块暴露的API,但B模块绝不允许调用A模块的API(IBM Knowledge Center)。
在类图中,依赖关系是指更改一个类(供应者)可能会导致更改另一个类(客户)。供应者是独立的,这是因为更改使用者并不会影响供应者。
例如,Cart 类依赖于 Product 类,因为 Product 类被用作 Cart 类中的“添加”操作的参数。在类图中,依赖关系是从 Cart 类指向 Product 类。换句话说,Cart 类是使用者元素,而 Product 类是供应者元素。更改 Product 类可能会导致更改 Cart 类。
在类图中,C/C++ 应用程序中的依赖关系将两个类连接起来,以指示这两个类之间存在连接,并且该连接比关联关系更加具有临时性。依赖关系是指使用者类执行下列其中一项操作:

  • 临时使用具有全局作用域的供应者类,
  • 将供应者类临时用作它的某个操作的参数,
  • 将供应者类临时用作它的某个操作的局部变量,
  • 将消息发送至供应者类。

模块之间产生依赖的主要方式是数据引用和函数调用。检验模块依赖程度是否合理,则主要看“变更”的容易程度。软件模块之间的调用方式可以分为三种:同步调用、回调和异步调用(异步消息的传递-回调机制)。同步调用是一种单向依赖关系。回调是一种双向依赖关系。异步调用往往伴随着消息注册操作,所以本质上也是一种双向依赖。

有一种观点将“依赖”直接总结为人脑中的依赖(为什么“耦合”概念该要摒弃)。文中提到:

只要程序员编写模块A时,需要知道模块B的存在,需要知道模块B提供哪些功能,A对B依赖就存在。甚至就算通过所谓的依赖注入、命名查找之类的“解耦”手段,让模块A不需要import B或者include “B.h”,人脑中的依赖仍旧一点都没有变化。唯一的作用是会骗过后文会提到的代码打分工具,让工具误以为两个模块间没有依赖。

代码的复杂度更主要的体现在阅读和理解,如果只是纠结于编译器所看到的依赖,实在是分错了主次,误入了歧途。

请我喝杯咖啡吧~

支付宝
微信