可扩展性到底有多重要

可扩展性(extensibility)是软件的一种非功能性特征,

维基百科上是这样解释的,

Extensibility is a software engineering and systems design principle where the implementation takes future growth into consideration.

可扩展性,是将软件未来的发展过程也考虑在内,而进行的一种设计选择。


1. 高可扩展性系统

我们遇到过很多具有高度可扩展的软件系统。


例如,以往著名的前端库jQuery,它采用了微内核架构,

微内核架构模式可以通过插件的形式添加额外的特性到核⼼系统中,这提供了很好的扩展性,也使得新特性与核⼼系统隔离开来。

—— 《软件架构模式

使用微内核架构,用户代码就可以通过插件的形式,集成到已有的核心系统中。


jQuery依照它强大的扩展性,以用户自定义jQuery插件的方式,

让前端开发工作重新繁荣了起来。


另一个对我印象深刻极具扩展性的例子是Scheme语言,

它是一门力求简洁的Lisp方言,

Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.

—— 《RnRS


为了实现可扩展性,Lisp宏的作用功不可没,

和通常的编程语言不同的是,

它除了可以自定义函数,还可以自定义语言的控制结构。


实际上,Lisp的用户代码可以对读取器和编译器进行扩展,

影响它们处理代码的方式,因此具备了强大的元编程能力。


类似这样,具有高度可扩展性的系统还有很多,

例如,Scala语言,微服务架构,koa框架中的middleware,等等。


但是在实际设计软件的时候,应该如何权衡系统的可扩展性呢?

真的是可扩展性越高越好吗?


2. 弊端

具有高度可扩展性的系统,我们用起来确实很好,

但是,却不见得我们也要有这样的设计。


2.1 过度设计

这有两方面的因素,

其一,我们未必有能力设计出,对未来考虑足够周全的系统。

实际项目中,通常是引入了不恰当的抽象概念,导致过度设计。


这通常是由于开发者过分自信导致的,

60% 的程序员认为自己是属于天才和良好类,只有 30% 左右的人认为他们是一般的程序员。

但实际上,只有不到 1% 的程序员属于天才程序员,有 10% 的良好程序员,其余大量的程序员是一般程序员。

—— 《屋中的大象


我们能欣赏漂亮设计的优雅之处,不代表我们也能做出这样的设计。


2.2 成本和适用性

其二,我们不确定现实项目是否需要这样的设计。

采取任何决策,都不是免费的,总会有各方成本的权衡。

况且,可扩展性设计,在不恰当的场景中,还会有自己的弊端。


例如,前文介绍的插件式微内核架构,当各个插件需要互相通信时,

就会遇到严重的系统集成性问题,

即,单独使用不会出现问题,当集成到一起,就容易出现各种冲突。


这是我们不愿意见到的。


又例如,Lisp语言虽然具有高可扩展性,但是却对它的用户提出了很高要求,

Lisp引入了一件麻烦事,称之为“新语言问题”。

如果对Lisp宏使用不当,会极大的提高代码库的使用和维护成本。


因此,在讨论是否需要可扩展性设计的时候,我们应当摒弃心中的执念,

以实际项目环境,以及人员构成为出发点,客观的考虑。


2.3 以柔性设计为名

最后,可扩展性设计,可看作是一种面向未来的柔性设计(supple design),

为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于做出修改,这就是柔性设计。然而,很多过度设计借着柔性设计的名义而自以为是正当的,结果过多的抽象层和间接设计常常成为项目的绊脚石。

—— 《领域驱动设计


因此,在考虑可扩展性时,不妨问一问自己,

我们真的对未来考虑清楚了么,还是暂时以可扩展性为由搪塞自己?

我们真的考虑好可扩展性的代价和弊端了么,还是只看到了它所带来的好处?


3. 技术本身的局限性

3.1 兼容性问题

软件一旦发布,对于后续升级来说,就容易产生令人头痛的兼容性问题,


DLL地狱(DLL Hell)指在Microsoft Windows系统中,

因为动态链接库(DLL)的版本或兼容性的问题而造成程序无法正常运行。


Windows早期并没有很严谨的DLL版本管理机制,

以致经常发生安装了某软件后,因为其覆盖了系统上原有的同一个DLL档,

而导致原有可运行的程序无法运行,

可是,还原回原有的DLL档之后,新安装的软件就无法运行。


这经常被人们称之为 DLL地狱(DLL Hell)。


Linux 系统中也遇到共享库(Shared Library)的兼容问题,

共享库的开发者不停的更改共享库的版本,以修正原有的Bug,增加新的功能或改进性能等。


由于动态链接的灵活性,使得程序本身和程序所依赖的共享库可以分别独立开发和更新,

但是共享库版本的更新可能会导致接口的更改或删除,这可能导致依赖于该共享库的程序无法正常运行。


由于发布后的软件,并不清楚自己被哪些其他系统所依赖,

所以,一旦进行了不兼容性的修改,难免有潜在的问题发生。


3.2 依赖管理

这个问题,以前和朋友们讨论过,但是第一次在公开场合看到有人提及是这篇文章,

[译] 超大型 JavaScript 应用的设计哲学Youtube),

作者提到了一个“enhance”的概念,把依赖关系倒置了,

把“我依赖xxx”倒置为,“我决定被xxx依赖”。


这样对于自己的任何修改都将是安全的了,虽然也会带来其他的问题,这里就不谈了。

毕竟这也是除了进行版本控制之外的另一种依赖管理办法。


3.3 工程

有了对兼容性的理解之后,本文想引出一个观点,

那就是,虽然从技术手段上可以削减软件变更带来的外部影响,

但是我认为,究其根本,软件变更的影响范围管理,却是一个软件工程问题。


高可扩展性诚然重要,但是工程上有一套合适的接口变更方案更重要。

如果变更是无法避免的话,那么与其避免发生,倒不如对它进行疏导和管控。


历史上大禹治水的典故,也有类似想法的写照,

禹从鲧治水的失败中汲取教训,改变了"堵"的办法,对洪水进行疏导。


结语

我们都期望当前维护的系统具有高度可扩展性,以减少眼前的工作负担,

但是,一方面这类系统实际上需要很强的预见性才能设计出来,难免出现偏差。

另一方面,高可扩展性也未必没有弊端,是否具有优势也与当前的项目场景有关。


此外,除了从技术角度考虑软件本身的设计和实现之外,

我们还可以跳出来,从工程角度来重新看待这个问题。

有时候,维护一个高可扩展性的软件系统,可能反而不如简单系统外加合适的变更方案更有效。


毕竟,目前软件都是由人来参与的,

并用来解决人的问题。