gitbook/软件设计之美/docs/265128.md

225 lines
14 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 26 | 简单设计:难道一开始就要把设计做复杂吗?
你好!我是郑晔。
从专栏开始到现在,关于软件设计,我们已经聊了很多。在学习设计原则和模式这个部分时,我们看着每次的代码调整,虽然结果还不错,但不知道你脑子之中有没有闪过这样的疑问:
如果我的每段代码都这么写,会不会把设计做复杂了呢?
确实,几乎每个人在初学设计的时候,都会有用力过猛的倾向。如何把握设计的度,是每个做设计的人需要耐心锤炼的。所以,行业里有人总结了一些实践原则,给了我们一些启发性的规则,帮助我们把握设计的度。
我把这些原则放到这个部分的最后来讲,是因为它们并不是指导你具体如何编码的原则,它们更像是一种思考方法、一种行为准则。
好,我们就来看看这样的原则有哪些。
## KISS
KISS原则是“Keep it simple, stupid”的缩写也就是保持简单、愚蠢的意思。它告诫我们对于大多数系统而言和变得复杂相比**保持简单能够让系统运行得更好**。
很多程序员都知道这条原则然而很少人知道这条原则其实是出自美国海军。所以它的适用范围远比我们以为的程序员社区要广泛得多。无论是制定一个目标还是设计一个产品抑或是管理一个公司我们都可以用KISS作为一个统一的原则指导自己的工作。
这个原则看起来有点抽象,每个人对它都会有自己理解的角度,所以,每个人都会觉得它很有道理,而且,越是资深的人越会觉得它有道理。因为资深的人通常都是在自己的工作领域中,见识过因为复杂而引发的各种问题。比如说,堆了太多的功能,调整起来很费劲这样的情况。我们在专栏前面讲过的各种问题,很多时候都是由于复杂引起的。
所以,对资深的人来说,保持简单是一个再好不过的指引了。其实,每个人都可以针对自己的工作场景给出自己的阐释,比如:
* 如果有现成的程序库,就不要自己写;
* 能用文本做协议就别用二进制;
* 方法写得越小越好;
* 能把一个基本的流程打通,软件就可以发布,无需那么多的功能;
* ……
这种级别的原则听上去很有吸引力但问题是你并不能用它指导具体的工作。因为怎么做叫保持简单怎么做就叫复杂了呢这个标准是没办法确定的。所以有人基于自己的理解给出了一些稍微具体一点的原则。比如在软件开发领域你可能听说过的YAGNI和DRY原则。
## YAGNI
YAGNI 是“You arent gonna need it”的缩写也就是你用不着它。这个说法来自于极限编程社区Extreme Programming简称 XP我们可以把它理解成**如非必要,勿增功能**。
我们在开篇词里就说过,软件设计对抗的是需求规模。一方面,我们会通过自己的努力,让软件在需求规模膨胀之后,依然能有一个平稳的发展;另一方面,我们还应该努力地控制需求的规模。
YAGNI就告诫我们其实很多需求是不需要做的。很多产品经理以为很重要的功能实际上是没什么用的。人们常说二八原则真正重要的功能大约只占20%80%的功能可能大多数人都用不到。做了更多的功能,并不会得到更多的回报,但是,做了更多的功能,软件本身却会不断地膨胀,变得越发难以维护。
所以在现实世界中我们经常看到一些功能简单的东西不断涌现去颠覆更复杂的东西。比如虽然Word已经很强大了但对于很多人而言它还只是一个写字的工具甚至它的重点排版功能都用得非常少。
于是这就给了Markdown一个机会。它可以让我们专注写内容而且简单的排版标记在日常沟通中也完全够用。至少我已经不记得自己上一次用Word写东西是什么时候了。
我在[《10x 程序员工作法》](http://https://time.geekbang.org/column/intro/100022301)里写的大部分内容实际上就是告诉你什么样的做法可以规避哪些的不必要功能。通过这里的介绍我们不难发现YAGNI是一种上游思维就是尽可能不去做不该做的事从源头上堵住。从某种意义上说它比其他各种设计原则都重要。
## DRY
DRY是“Dont repeat yourself”的缩写也就是**不要重复自己**。这个说法源自Andy Hunt和Dave Thomas的《程序员修炼之道》The Pragmatic Programmer。这个原则的阐述是这样的
> 在一个系统中,每一处知识都必须有单一、明确、权威地表述。
> Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
每个人对于DRY原则的理解是千差万别的最浅层的理解就是“不要复制粘贴代码”。不过两个作者在二十年后的第二版特意强调这个理解是远远不够的。**DRY针对的是你对知识和意图的复制**。它强调的是,在两个不同地方的两样东西表达的形式是不同的,但其要表达的内容却可能是相同的。
我从《程序员修炼之道》中借鉴了一个例子,看看我们怎么在实际的工作中运用 DRY 原则。下面是一段打印账户信息的代码,这种写法在实际的工作中也非常常见:
```
public void printBalance(final Account account) {
System.out.printf("Debits: %10.2f\n", account.getDebits());
System.out.printf("Credits: %10.2f\n", account.getCredits());
if (account.getFees() < 0) {
System.out.printf("Fees: %10.2f-\n", -account.getFees());
} else {
System.out.printf("Fees: %10.2f\n", account.getFees());
}
System.out.printf(" ----\n");
if (account.getBalance() < 0) {
System.out.printf("Balance: %10.2f-\n", -account.getBalance());
} else {
System.out.printf("Balance: %10.2f\n", account.getBalance());
}
}
```
然而,在这段代码中,隐藏着一些重复。比如,对负数的处理显然是复制的,可以通过增加一个方法消除它:
```
String formatValue(final double value) {
String result = String.format("%10.2f", Math.abs(value));
if (value < 0) {
return result + "-";
} else {
return result + " ";
}
}
void printBalance(final Account account) {
System.out.printf("Debits: %10.2f\n", account.getDebits());
System.out.printf("Credits: %10.2f\n", account.getCredits());
System.out.printf("Fees:%s\n", formatValue(account.getFees()));
System.out.printf(" ----\n");
System.out.printf("Balance:%s\n", formatValue(account.getBalance()));
}
```
还有,数字字段格式也是反复出现的,不过,格式与我们抽取出来的方法是一致的,所以,可以复用一下:
```
String formatValue(final double value) {
String result = String.format("%10.2f", Math.abs(value));
if (value < 0) {
return result + "-";
} else {
return result + " ";
}
}
void printBalance(final Account account) {
System.out.printf("Debits: %s\n", formatValue(account.getDebits()));
System.out.printf("Credits: %s\n", formatValue(account.getCredits()));
System.out.printf("Fees:%s\n", formatValue(account.getFees()));
System.out.printf(" ----\n");
System.out.printf("Balance:%s\n", formatValue(account.getBalance()));
}
```
再有,这里面的打印格式其实也是重复的,如果我要在标签和金额之间加一个空格,相关的代码都要改,所以,这也是一个可以消除的重复:
```
String formatValue(final double value) {
String result = String.format("%10.2f", Math.abs(value));
if (value < 0) {
return result + "-";
} else {
return result + " ";
}
}
void printLine(final String label, final String value) {
System.out.printf("%-9s%s\n", label, value);
}
void reportLine(final String label, final double value) {
printLine(label + ":", formatValue(value));
}
void printBalance(final Account account) {
reportLine("Debits", account.getDebits());
reportLine("Credits", account.getCredits());
reportLine("Fees", account.getFees());
System.out.printf(" ----\n");
reportLine("Balance", account.getBalance());
}
```
经过这样的修改如果我们要改金额打印的格式就去改formatValue方法如果我们要改标签的格式就去改reportLine方法。
可能对于有的人来说,这种调整的粒度太小了。不过,我想说的是,如果你的感觉是这样的话,证明你看问题的粒度太大了。
如果仔细品味这个修改,你就能从中感觉到它与我们之前说的分离关注点和单一职责原则有异曲同工的地方,没错,确实是这样的。在讲分离关注点和单一职责原则的时候,我强调的重点也是**粒度要小**。这个例子从某种程度上说,也是为它们增加了注脚。
虽然我们在这里讲的是代码但DRY原则并不局限于写代码比如
* 注释和代码之间存在重复,可以尝试把代码写得更清晰;
* 内部API在不同的使用者之间存在重复可以通过中立格式进行API的定义然后用工具生成文档、模拟 API 等等;
* 开发人员之间做的事情存在重复,可以建立沟通机制降低重复;
* ……
所有这些努力都是在试图减少重复,同时也是为了减少后期维护的成本。
## 简单设计
上面说的这三个原则都是在偏思维方式的层面而下面这个原则稍稍往实际的工作中靠了一些它就是简单设计Simple Design原则。
这个原则来自极限编程社区它的提出者是Kent Beck这个名字在我的两个专栏中已经出现了很多次由此可见他对现代软件开发的影响很大
简单设计之所以叫简单设计因为它只包含了4条规则
* 通过所有测试;
* 消除重复;
* 表达出程序员的意图;
* 让类和方法的数量最小化。
这4条规则看起来很简单但想做到对于很多人来说是一个非常大的挑战。Kent Beck是极限编程这种工作方式的创始人所以想满足他提出的简单设计原则最好要做到与之配套的各种实践。
我们来逐一地看下每条规则。第1条是**保证系统能够按照预期工作**,其实,这一点对于大多数项目而言,已经是很高的要求了。怎么才能知道系统按照预期工作,那就需要有配套的自动化测试。大多数项目并不拥有自己的自动化测试,更何况是在开发阶段使用的单元测试,尤其是还得保证测试覆盖了大多数场景。
在XP实践中想要拥有这种测试最好是能够以测试驱动开发Test Driven Development简称 TDD的方式工作。而你要想做好TDD最根本的还是要懂设计否则你的代码就是不可测的想给它写测试就是难上加难的事情。
后面3条规则其实说的是**重构的方向**而重构也是XP的重要实践。第2条消除重复正如前面讲DRY原则所说的你得能够发现重复这需要你对分离关注点有着深刻的认识。第3条表达出程序员的意图我们需要编写有表达性的代码这也需要你对“什么是有表达性的代码”有认识。我们在讲DSL曾经说过代码要说明做什么而不是怎么做。
第4条让类和方法的数量最小化则告诉我们不要过度设计除非你已经看到这个地方必须要做一个设计比如留下适当的扩展点否则就不要做。
但是,有一点我们需要知道,能做出过度设计的前提,是已经懂得了设计的各种知识,这时才需要用简单设计的标准对自己进行约束。所以,所谓的简单设计,对大多数人而言,并不“简单”。
我们前面说了简单设计的理念来自于极限编程社区这是一个重要的敏捷流派。谈到敏捷很多人以为做敏捷是不需要设计的其实这是严重的误解。在敏捷实践的工程派也就是XP这一派中如果单看这些实践的步骤你都会觉得都非常简单无论是TDD也好抑或是重构也罢如果你没有对设计的理解任何一个实践你都很难做好。
没有良好的设计代码就没有可测试的接口根本没有办法测试TDD也就无从谈起。不懂设计重构就只是简单的提取方法改改名字对代码的改进也是相当有限的。
简单设计是Kent Beck这样的大师级程序员在经历了足够的积累返璞归真之后提出的设计原则它确实可以指导我们的日常工作但前提是我们需要把基础打牢。片面地追求敏捷实践而忽视基本功往往是舍本逐末的做法。
## 总结时刻
今天,我给你讲了一些启发性的编程原则,这些设计原则更像是一种思考方式,让我们在软件设计上有更高的追求:
* KISS原则Keep it simple, stupid我们要让系统保持简单
* YAGNI原则You arent gonna need it不要做不该做的需求
* DRY原则Dont repeat yourself不要重复自己消除各种重复。
我们还讲了一个可以指导我们实际工作的简单设计原则它有4条规则
* 通过所有测试;
* 消除重复;
* 表达出程序员的意图;
* 让类和方法的数量最小化。
软件设计相关的基础内容,到这里,我已经全部给你讲了一遍。然而,你可能会有疑问,有了这些东西之后,我该如何用呢?从下一讲开始,我们来聊聊,如果有机会从头开始的话,该如何设计一个软件。
如果今天的内容你只能记住一件事,那请记住:**简单地做设计**。
![](https://static001.geekbang.org/resource/image/c4/f9/c455311f514e9d66f830597ba7a5c2f9.jpg)
## 思考题
最后,我想请你分享一下,你还知道哪些让你受益匪浅的设计原则,欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。