解耦设计手法小结
设计是一个平衡的产物,需要在各个约束条件下(组织目标,业务目标,开发流程,技术能力,学习及维护成本等)不断地进行演进。 我们虽然不提倡做大而全的设计,但会坚持进行基础性设计,以保证我们的设计一直在正确的方向上演进。
设计演进的过程既可以是自上而下的,也可以是自下而上的。
基本设计原则
业界普遍被接受的设计原则不再赘述。这里特别针对基于开源项目的软件,其总体主旋律将是:跟随,扩展,贡献,其中跟随将是一个基本能力,反观深度定制的方式会遭遇越来越多的尴尬。落实在设计上,其最核心的设计原则:隔离自有业务。相较于模块化的低耦合、高内聚的原则,这里的要求会更高。
先从模块上考虑应用的层次,依次考虑:
- 应用层
- 开源项目本身的定制或移植机制
- 新增的接口层
- 新增的适配层或业务层
- 既有的接口层
- 既有的实现
设计本身还要保证业务的完整性,以及对性能、系统开销、卡顿和稳定性的要求。
解耦的设计实践
以下为关于解耦的设计方法总结,以及应用要点便于在设计时评估。
解耦是隔离变化和降低复杂度的重要手段,这里以解耦代言隔离变化,其思想就是以分工协作代替全面控制,接口的定义大于业务逻辑的定义。其思考路径是:分不分?如何分?如何分是具体形式的问题,下面详述。分不分则取决于功能需求, 常见分离的需求有:
- 功能强内聚
这没什么好说的, 最常见的理由。 - 功能的整合和转换
就是为了整合某些功能或者达到某种切换的目的,向上提供一个更为标准统计的接口。内部可能会进行一些业务逻辑处理,数据、状态转换之类的操作。如编译器分出前后端也是这样的概念。 - 降低复杂度
接口的定义至关重要,接口本身不能绑定业务约束或者流程。整体交互上是面向无状态的接口,而不是面向过程。过程的合理性,即业务流程则由不同的单元内部保障,再通过接口交互。而向约束的编程也是在函数内进行约束的判断,间接达到带状态接口。
在手法上可以概括为从宏观到微观的四个层次:
- 进程 也可以是物理空间上的分离
- 库 模块化/分层
- 代码 如下图:
进程
以分进程的方法来进行协作是 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 大叔的解释:
- 首先职责的定义是: 引起变化的原因,不是由分类所决定的。如果存在相对的变化,才要考虑分离。
- 其次,关于引起变化的因素,不要空想。一定确信有变化的可能,才会加以考虑。
他的提醒是非常中肯的。实践中正是常常基于功能的分类来定义职责的。举个例子,假如我们要开发一个学校的教职员工管理系统。需要定义一个教师员工的类(炒菜的师傅先就不考虑了),考虑到老师和班主任两个角色,通常会认为他有两类职责:
- 教师 (班主任很可能会带课)
- 班级的管理 (组织班委,整治一下早恋之类的)
这时你拿着设计到了一个寄宿学校,校长可能会告诉你,他们这里的教师会轮流值班,兼做保育员,照看住校的学生。又是一个新的职责,怎么办?
如果遵守单一职责的原则,我们应该增加一个接口:
果真要如此吗? 注意,如果是在一般的学校,保育员不是老师的本职工作,可在这所寄宿学校里,却是教师的本职工作,是和老师一起变化的。校长的反馈是:
“我们学校的教师必须担任保育工作,我并不认为这会是什么新职责。作为教师,要么接受,要么离开。至于班主任工作,确实还是其特殊的地方,不然也不会给担任班主任的老师多一点津贴了。”
请再体会一下,关于保育员职责的讨论。如果两个职责/角色不是同时变化的,才考虑分离。 如果确定同时变化,就没有必要分离。除非有一天,某个劳动部门到该寄宿学校检查,认为他们这样不符合某个法律规定,强制规定老师可以选择是否担当保育员。如此一来,两个职责就又变成独立变化的了,就可以考虑分离职责。
再进一步,如果是针对一个只有一个支教教师的小学,极为偏僻。这里的校长会告诉你:
”这个学校里的每一个教师,唯一的一个,既是校长,也是老师。我不认为还需要明确班主任做什么,教师做什么,在这里,只要学生需要的都要做。并且这里很穷,五年内都不见得再有新老师来。”
这个感人的故事告诉我们,在这所学校里, 他不在了,这所学校也就不在了,完全没有什么相对的变化,也没有什么可以确认的变化。所以在这里的管理系统里,教职员工只有一个单实例类,:
聊到这里,不知道我说清楚了没有!设计要跟着需求走,不能生硬的套理论。欢迎拍砖!