解耦设计手法小结

设计是一个平衡的产物,需要在各个约束条件下(组织目标,业务目标,开发流程,技术能力,学习及维护成本等)不断地进行演进。 我们虽然不提倡做大而全的设计,但会坚持进行基础性设计,以保证我们的设计一直在正确的方向上演进。

设计演进的过程既可以是自上而下的,也可以是自下而上的。

基本设计原则

业界普遍被接受的设计原则不再赘述。这里特别针对基于开源项目的软件,其总体主旋律将是:跟随,扩展,贡献,其中跟随将是一个基本能力,反观深度定制的方式会遭遇越来越多的尴尬。落实在设计上,其最核心的设计原则:隔离自有业务。相较于模块化的低耦合、高内聚的原则,这里的要求会更高。

先从模块上考虑应用的层次,依次考虑:

  • 应用层
  • 开源项目本身的定制或移植机制
  • 新增的接口层
  • 新增的适配层或业务层
  • 既有的接口层
  • 既有的实现

设计本身还要保证业务的完整性,以及对性能、系统开销、卡顿和稳定性的要求。

解耦的设计实践

以下为关于解耦的设计方法总结,以及应用要点便于在设计时评估。

解耦是隔离变化和降低复杂度的重要手段,这里以解耦代言隔离变化,其思想就是以分工协作代替全面控制,接口的定义大于业务逻辑的定义。其思考路径是:分不分?如何分?如何分是具体形式的问题,下面详述。分不分则取决于功能需求, 常见分离的需求有:

  • 功能强内聚
    这没什么好说的, 最常见的理由。
  • 功能的整合和转换
    就是为了整合某些功能或者达到某种切换的目的,向上提供一个更为标准统计的接口。内部可能会进行一些业务逻辑处理,数据、状态转换之类的操作。如编译器分出前后端也是这样的概念。
  • 降低复杂度
    接口的定义至关重要,接口本身不能绑定业务约束或者流程。整体交互上是面向无状态的接口,而不是面向过程。过程的合理性,即业务流程则由不同的单元内部保障,再通过接口交互。而向约束的编程也是在函数内进行约束的判断,间接达到带状态接口。

在手法上可以概括为从宏观到微观的四个层次:

  • 进程 也可以是物理空间上的分离
  • 库 模块化/分层
  • 代码 如下图:
    解耦设计手法小结

进程

以分进程的方法来进行协作是 Unix 世界的传统,即 KISS 原则。Unix 下有各式小工具,这些工具之间通过管道连结起来达到强大的功能。另外以服务的方式隔离业务也很常见。如 Windows 中 COM+ 的架构,甚至是 HTTP Server 等。

分进程的特点在于不同进程间的功能高度独立,并行处理的情况较多,服务提供者能够按需布署,存在一对多的情况,或有额外的安全性考虑。而挑战在于性能、系统开销,需要熟悉 IPC 以及共享内存的知识。

分库

这是一个重要的模块化手法,主要是以动态库和脚本的形式, 甚至是独立的程序提供扩展。其核心思想是以插件的形式完成功能组装,以物理分离的形式提供出来。
插件本身实现一套标准的接口,包括:参数配置,接收输入,状态输出,数据输出等。如 Windows 的核心驱动模块,Photoshop/GIMP 中的图像处理功能,Matlab 以及 R 语言中的函数库等等,不胜枚举。以静态库形式提供出来的模块,更接近于代码级或者分层级别的体现,无法直接达到按需布署的能力。

分库要求各个独立库的接口层比较单一,特别适用于业务逻辑强内聚的场景。同时插件的功能将直接影响主程序的稳定性。

分层

就是将某一类功能的类和代码集中起来,向外提供特定接口或若干接口类,这个逻辑上的集合,就是层(layer)或者模块(module),也有叫 unit 或者 API 之类的。与分库、分进程本质区别就在于它是一个逻辑集合,优势在于可以更灵活的与不同模块交互,因为接口可以多样化(支持代码级的交互),这也同样是它的劣势,有时导致它形同虚设,丧失了解耦的能力。

所以分层成功与否,关键在于接口(含接口类)的定义和控制。常见的一些手法,如MVC, 胶合层(glue),适配层(port),WebKit 和 Chrome 中也有应用。

代码

这是一个最为微观,最为复杂的层次。但是到了这层,并不表示必然存在耦合问题。如果一些架构在设定上考虑到了扩展和适配的需求,在这个级别进行解耦反而最为自然。

WebKit 的 port 方案

以 Image 类为例,它的一个函数与平台相关,于是类的实现被放在了三个文件中 Image.cpp, ImageMac.mm 和 ImageWin.cpp中。Image.cpp 中实现了公共的部分,而 ImageMac.mm 中实现了 Mac OS版本,而ImageWin.cpp 则实现了 Windows 版本。
解耦设计手法小结
另一种实现方式,如多媒体元素的播放控件。首先是 MediaPlayer 提供了一部分公共的逻辑,对于与平台相关实现的部分,定义了一个 MediaPlayerPrivateInterface,各平台继承自这个接口实现各自的逻辑。
解耦设计手法小结
当我们解决了架构上的解耦后,在模块内部引入一定的耦合度就不是问题了。可供选择的方法就太多了。

Helper Class

Helper Class 已经为一个类添加一个友类,执行一些差异化的业务。Helper Class 可以使用类似 WebKit Port 的机制为不同的系统提供不同的实现,也可以配合工厂模式,实现更为弹性的选择。
解耦设计手法小结分散的逻辑判断就可以转为函数调用。
解耦设计手法小结关于 helper 的使用一直是有争议,网上也有很多避免使用 helper class 的讨论。 主要论调在于认为 helper class 是过程化的产物,思考时是考虑的是流程上的逻辑补充。

[OOD] 为什么单一职责原则(SRP)是最难运用的

单一职责原则(SRP)已经几乎是每一个程序员都知道的设计原则。最早由 Robert C. Martin 在《敏捷软件开发 — 原则、模式与实践》中正式提出。书中作者在结论中提到:

SRP是所有设计原则最简单的,但也是最难运用的。(中文翻译有之一,略去了)

现实工作中,关于一个类是否符合 SRP,或者是否有必要符合 SRP 的讨论是经常发生的。争论的关键在于职责的定义,但我理解 SRP 真正的核心是关注于变化。这并不是我的新见解,全是来自 Martin 大叔的解释:

  • 首先职责的定义是: 引起变化的原因,不是由分类所决定的。如果存在相对的变化,才要考虑分离。
  • 其次,关于引起变化的因素,不要空想。一定确信有变化的可能,才会加以考虑。

他的提醒是非常中肯的。实践中正是常常基于功能的分类来定义职责的。举个例子,假如我们要开发一个学校的教职员工管理系统。需要定义一个教师员工的类(炒菜的师傅先就不考虑了),考虑到老师和班主任两个角色,通常会认为他有两类职责:

  • 教师 (班主任很可能会带课)
  • 班级的管理 (组织班委,整治一下早恋之类的)

解耦设计手法小结
这时你拿着设计到了一个寄宿学校,校长可能会告诉你,他们这里的教师会轮流值班,兼做保育员,照看住校的学生。又是一个新的职责,怎么办?

如果遵守单一职责的原则,我们应该增加一个接口:
解耦设计手法小结
果真要如此吗? 注意,如果是在一般的学校,保育员不是老师的本职工作,可在这所寄宿学校里,却是教师的本职工作,是和老师一起变化的。校长的反馈是:

“我们学校的教师必须担任保育工作,我并不认为这会是什么新职责。作为教师,要么接受,要么离开。至于班主任工作,确实还是其特殊的地方,不然也不会给担任班主任的老师多一点津贴了。”

请再体会一下,关于保育员职责的讨论。如果两个职责/角色不是同时变化的,才考虑分离。 如果确定同时变化,就没有必要分离。除非有一天,某个劳动部门到该寄宿学校检查,认为他们这样不符合某个法律规定,强制规定老师可以选择是否担当保育员。如此一来,两个职责就又变成独立变化的了,就可以考虑分离职责。

再进一步,如果是针对一个只有一个支教教师的小学,极为偏僻。这里的校长会告诉你:

”这个学校里的每一个教师,唯一的一个,既是校长,也是老师。我不认为还需要明确班主任做什么,教师做什么,在这里,只要学生需要的都要做。并且这里很穷,五年内都不见得再有新老师来。”

这个感人的故事告诉我们,在这所学校里, 他不在了,这所学校也就不在了,完全没有什么相对的变化,也没有什么可以确认的变化。所以在这里的管理系统里,教职员工只有一个单实例类,:
解耦设计手法小结
聊到这里,不知道我说清楚了没有!设计要跟着需求走,不能生硬的套理论。欢迎拍砖!