消除重复与可复用相距多远

程序员们都讨厌重复,

因为他们知道,

这是可以避免的冗余工作量。

——代码坏味


如果本来就不可避免,

那只好认了,

就好像需求不可能不变的事实一样。


但是,盲目的消除重复

并不是正确的姿势,

反而会把简单的事情搞复杂。


为什么程序员们甘心写那么多“public class XXX {}”,

甘心写那么多“END”,“;”或者“)”,

而不设法避免呢?


为什么不把所有的“this”都提取出来,

放在一个地方,

在编译前,再把他们塞入代码中呢。


诚然,通过macro,

可以做一些代码生成之类的事情,

但是直觉告诉我们,不能乱来。


原因是什么?

消除重复与面向复用其实还相距太远。


什么是面向复用的编程

“面向”,指的是目的,目标。


方法论告诉我们,

要完成一件事需要3个步骤,

(1)确定目标

(2)分析差距

(3)缩小差距


确定目标是第一步。


要想让我们的程序被复用,

首先他要设计成可以被复用的,

不是哪天要用了,忽然就行了。


而且,在一套软件中,

并不是所有的代码都是可复用的,

有些根本无法复用。


承认了这两个事实以后,

任务一下子简单了很多。


提取并不意味着抽象

一段代码,在很多地方都出现了,

我们马上就能想到,

把它提取到了一个函数中。


然而这对可复用并没有任何作用,

我们只是凑巧成功了几次。


假如某个调用处的业务逻辑发生了变化,

我们就遇到麻烦了,

不能修改提取出去的代码,因为别人也在用,

还要把代码再复制回去,只调整这一块。


自讨苦吃啊。

那可如何是好?


这是因为,提取和抽象是不同的,

提取是文本层次的观察,

而抽象是逻辑层次的考察。


这段代码虽然在很多地方出现了,

可是它却由不同的逻辑单元组成,

这就隐藏了很多引起它改变的因素。

——单一职责原则


我们应该先把这些逻辑单元封装好,

然后拼装出统一的抽象接口,

各处对接口进行调用。

——依赖倒置原则

——合成/聚合复用原则


一旦某个逻辑单元需要调整了,

那么只需要单独为某处调用,

实现一个新的接口函数就行了。

——开-闭原则


外表要简单,内心不要太简单

有人觉得有个拼装层太麻烦了,

还不如直接提取函数好呢,

这其实是混淆了抽象与实现。


接口的使用者是看不到实现方式的,

实现很复杂是为了有更高的灵活性。

——迪米特法则


语言的魅力不就是,把复杂藏于只言片语之后吗?

编程的目的不就是,为多变的业务逻辑提供简洁的描述语言吗?

——领域特定语言


一切都应该尽可能地简单,但不要太简单。

——爱因斯坦


业务与功能

经常变动的是业务逻辑,

而可复用的是功能模块。


对逻辑我们要抽象,

而对功能我们要封装。


很多新手认为,

把相关的代码组织在一起就是封装了,

这与认为把代码提取出来了就是抽象了一样失败。


要封装,是因为它们“能够封装”,

而不是代码恰好出现在了一起。


只有与要描述的业务无关的功能,

才是可复用的单元。


每个页面都发送AJAX请求,

然后更新一个id="message"的标签,

这是不可封装的。


而发送AJAX模块,

更新任一标签内容的模块,

才是可封装的。


一致的业务流程,也是可以封装的。

例如,如果有定时任务,

可以封装一个定时任务管理器,

只需要挂载任务的配置信息就可以自动执行。


对了,别人有写好了的,

不用自己做,

但我们要这么想。


结语

能闻到坏味是好事,

但是不正确的打扫方法,

反而会使代码更难维护。


消除重复是表象,

而面向复用才是目的。


和别人读同样的书,

不一定考上一样的大学,

更不一定有相同的人生。