复用是人类文明发展的重要推动力。很难想象,一家制造汽车的企业需要从最基本的JK、RS等逻辑电路开始构造控制系统,更不可能自己去制造晶体硅。但是,在软件行业,复用(特别是业务层面的软件复用)并不像想象起来那么容易。

软件是信息制品。信息制品的典型特点是复用的边际成本极低。例如,一个设计良好的登录模块,既可以用于学生的学籍管理,也可以用于购物网站,还有你能想到的各种需要身份认证的场景。

尽管软件行业在框架层面的复用已经取得了突出的进步,如平台级的k8sk8s(Kubernetes)是一种开源的容器编排引擎。、框架级的SpringSpring是一种著名的Java开发编程框架。等,但是在业务层面,还有许多业务组件都必须从头写起,很难在不同的场景下复用。业务的丰富性、多样性固然是一个原因,但是设计边界和设计职责的不合理、过度复杂的依赖等,也是阻碍复用的重要因素。

通过提升设计质量,软件的复用能力可以得到极大的提升。概括来说,选择合适的复用粒度,定义清晰的设计职责和设计契约并很好地管理依赖,是提升代码复用能力的重要手段。

选择合适的复用粒度

在多大粒度上复用是一个有挑战性的问题。复用粒度越大,复用价值也就越大,不过复用的机会往往更小。所以,我们看到标准函数库很容易被复用,但是没有太多人提及这是一种“复用”——因为大家对这个操作已经习以为常,它给效率带来的提升是有限的。业务模块的复用价值很大,但很多时候难以被复用,因为总是会有那么一点看起来不明显的区别阻碍对业务模块的复用。

时至今日,业务模块的复用已经有了更好的理论基础,而且经过了实践的检验。这就是以领域为中心的设计。通过恰当的确定问题域的边界,如把一个订餐系统切分为用户、订单、支付、配送、消息通知等子域,并保持各个子域边界之间的抽象和隔离,就可以大幅提升问题的通用性,从而增加复用机会。通过这样的方式,可以发展出一大批专门的业务服务,已经很好地实现了商业化的短信发送、地图服务、聊天消息等就是其中的典型例子。本书的第4章和第8章将分别讨论如何进行领域建模及如何基于领域模型指导软件实现。

需要特别提及,代码的“复制-粘贴”不是复用,它是复用的反面。复用是一种几乎零成本的、在新场景下可安全使用既有设计资产的活动。尽管复制-粘贴代码似乎也节省了一点编码成本,却为以后的维护埋下了隐患。例如,如果后续代码在某些方面有能力增强,那么其他的副本要不要一起跟着修改?如果两段代码看起来很相似,仅有一点点不同,会不会增加阅读者的负担?一般来说,需要对代码进行复制-粘贴才能进行的所谓“复用”,是复用能力不足的表征。

清晰的设计职责和设计契约

可靠的复用必须满足两个条件。第一,被复用模块的职责必须清晰,这样别人才可以知道该不该复用、能不能复用。第二,被复用模块的实际行为必须和承诺的职责相一致,这样才能被可靠地复用。这就是清晰的设计职责和设计契约。本书第6章和第7章将深入讨论设计职责,第10章将讨论更为严谨而有效的契约式设计。

很好地管理依赖

在大多数情况下,软件模块需要依赖其他模块才能正常工作。拔出萝卜带出泥,是影响复用的一个很常见的问题。

我曾经见过一个软件系统,它早期基于微软的技术栈开发,代码中各处都充满了对微软的某个编程框架的依赖,如随处可见的AfxMessageBoxAfxMessageBox是早期Windows编程框架提供的一个消息提示API。。后来,当尝试把这个系统迁移到Linux环境中时,就不得不在各处做改动。

在软件设计中,很好地管理依赖是优秀软件工程师的基本功。例如,尽量依赖抽象的接口而不是具体的实现、依赖设计小而聚焦的接口而不是大而全的接口等。本书的第6章将会介绍依赖倒置、接口分离等设计原则,它们都是提高复用能力的有效手段。