Post

软件设计中的 DRY

DRY

软件设计中有一个非常简单的原则——Don’t Repeat Yourself(DRY),按维基百科的定义就是:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system

系统中的每一部分,都必须有一个 单一的、明确的、权威的表示。面向对象中也有一个原则,叫 一个规则,实现一次(One rule,one place),这两个定义是类似的,都是表示软件系统中的模块和功能的唯一性。DRY 最先出现在《程序员修炼之道》 中,后在软件设计中被广泛认可,并应用到数据库、测试、系统设计、文档等等领域。DRY 能够大大降低软件的维护成本,并且让软件的后期拓展变得更加容易。另外,DRY 很大程度上为我们设计 正交 的软件提供了方向。

我这里说简单是指它比较容易理解,但是要应用,真正践行是有一定难度的。下面我们探讨几个去除软件中重复元素的方式方法。

设计模式与泛型

设计模式可以帮助我们将重复或者是共性的部分抽象出来,这里举几个例子。

之前我们讲过的 策略模式 就是将模块中共性的行为提取出来,形成一个新的抽象类型,在去重的同时也实现了解耦,双赢。单例模式 是对运行时的对象进行去重,保证对象仅被创建一次。

此外,模版方法模式 通过限定程序执行的框架来把规避重复的步骤。让结构变得更加的清晰。迭代器模式 避免了迭代行为的重复。

例子还有很多,这里我就不赘述了。每当你发现代码中有重复的时候,不妨想想是否可以利用设计模式来让代码变得更好。

当然,设计模式大多应用在面向对象语言中,它仅仅是一个工具,最主要的还是这些模式背后的原则,比如 SOLID 原则好莱坞原则 等等,设计模式是 “术”,而它们背后的这些原则是 “道”,应用设计模式不是我们最终的目的,这些原则背后的思想才更值得我们深思,研究并反复琢磨这些原则能够帮助我们更加理解设计模式,使用起来也会游刃有余。

除了设计模式,泛型也是一个很好的工具。让不同类型的元素都可以使用同样的逻辑,这是泛型编程在努力践行的,当然要真正做到这一点,不容易。

代码生成

重复的元素很多时候不一定都出现在代码中。举个例子,数据库的 schema 定义往往就不在代码中,代码中只有操作数据库的对象。严格说来,数据库的操作对象和数据库的 schema 其实是同样的东西,都表示的是数据库的数据定义。想想看,如果数据库的 schema 发生变化,比如增加一个字段、删除一个字段、改变一个字段的类型,那么是不是意味着操作数据库的对象也要被更改,否则二者就不兼容了?这就产生了耦合。我这举的还是一个简单的例子,想象一下,在一个复杂的长长的数据 Pipeline 中,上游数据和下游数据之间该有多少这样的情况?上游的一个数据表发生变化,这整个 Pipeline 该有多少地方需要被修改?你可能会说,这还不简单,保证上游的数据不变不就行了吗?要知道变化才是软件开发常态,软件设计之所以难,核心原因就是这个世界是变化的,并且你都不知道以后会怎么变。

代码生成在这种情况下就能很好地发挥作用,我们可以写一个自动化的脚本程序来自动生成需要的代码逻辑。每当数据库 schema 发生变化,脚本程序就会被触发,程序脚本会根据 schema 的变化来更改现有的代码逻辑,反过来也是一样,代码中的数据对象发生变化,也会触发数据 schema 的变化,总之,最终不需要任何的人为操作,代码逻辑和数据库的 schema 定义自动保持一致。Pipeline 也是同样的道理,上游数据定义发生变化,下游的业务逻辑也会据此自动发生更改,当然,这里不一定需要脚本程序,下游的业务可以根据上游过来的数据自行定义所产生的数据格式。

这是一种更高级的去重方法,这里我们并没有删除任何一份重复逻辑,比如代码中的数据对象和数据库的 schema 都同时存在。我们的策略是通过代码生成将重复的两部分连接形成一个整体。我们仅需要维护这一个整体,并不需要关心这个整体是怎么形成的,这个整体内部的重复对我们来说也等于不存在。

代码生成适用的例子还有很多,比如一个大规模软件中可能存在不同语言的模块,当这些模块之间需要互相调用该如何处理呢?比方说一个 Java 的函数需要调用一个 C++ 的函数,难道说这个时候我们要将这个 C++ 函数用 Java 再重写一遍吗?这就又引入了重复。我们只需要抓住那些在整个软件中不变的东西,比如数据结构、消息格式等等,所有的模块对这些东西都有共识,基于这一点,调用执行脚本来执行特定的模块即可。

重构

其实我在想,很多时候在代码中去重,也无需特别复杂的工具或理念,很多很常见的重构方法也能帮助到我们,比如我之前阅读《重构》这本书总结的一些 方式方法。你看了就会知道,这里面列的很多技巧其实都是再直接不过的,比如把一个复杂的函数拆成多个小函数,把没有必要的抽象剔除,给对象中成员加上 getter 和 setter 方法,剔除一些没有必要的临时变量,这些方法我们或多或少都知道一些,只是说我们对它们的印象没有那么深刻,我们缺少的是实际的应用。把这些简单的技巧灵活应用,自己写过的程序反复看,反复改,久而久之也能写出好程序。想要做好其实不难,需要的是你有一个想要变好的心,以及持之以恒去做那些你认为对的事情。

This post is licensed under CC BY 4.0 by the author.