如何降低软件的复杂性?

作者: 阮一峰

日期: 2018年9月10日

John Ousterhout 是斯坦福大学计算机系教授,也是 Tcl 语言的创造者。

今年四月,他出版了一本新书《软件设计的哲学》(A Philosophy of Software Design)。这是课程讲稿,160多页,亚马逊全部是五星好评。

我还没读这本书,但是我看了作者在谷歌的一次演讲(Youtube),介绍了这本书的主要内容。我觉得非常值得看,大部分书教你怎么写正确的代码,这本书教你如何正确设计软件。

下面我就根据演讲视频和网上的书评,做一下笔记。

一、什么是复杂性

Ousterhout 教授认为,软件设计的最大目标,就是降低复杂性(complexity)。 所谓复杂性,就是任何使得软件难于理解和修改的因素。

Complexity is anything that makes software hard to understand or to modify.

复杂性的来源主要有两个:代码的含义模糊和互相依赖。

Complexity is caused by obscurity and dependencies.

模糊指的是,代码里面的重要信息,看不出来。依赖指的是,某个模块的代码,不结合其他模块,就会无法理解。

Obscurity is when important information is not obvious.

Dependency is when code can't be understood in isolation.

复杂性的危害在于,它会递增。你做错了一个决定,导致后面的代码都基于前面的错误实现,整个软件变得越来越复杂。"我们先把产品做出来,后面再改进",这根本做不到。

Complexity is incremental, the result of thousands of choices. Which makes it hard to prevent and even harder to fix.

二、复杂性的隔离

降低复杂性的基本方法,就是把复杂性隔离。"如果能把复杂性隔离在一个模块,不与其他模块互动,就达到了消除复杂性的目的。"

Isolating complexity in places that are rarely interacted with is roughly equivalent to eliminating complexity.

改变软件设计的时候,修改的代码越少,软件的复杂性越低。

Reduce the amount of code that is affected by each design decision, so design changes don't require very many code modifications.

复杂性尽量封装在模块里面,不要暴露出来。如果多个模块耦合,那就把这些模块合并成一个。

When a design decision is used across multiple modules, coupling them together.

三、接口和实现

模块分成接口和实现。接口要简单,实现可以复杂。

Modules are interface and implementation. The best modules are where interface is much simpler than implementation.

It's more important for a module to have a simple interface than a simple implementation.

好的 class 应该是"小接口,大功能",糟糕的 class 是"大接口,小功能"。好的设计是,大量的功能隐藏在简单接口之下,对用户不可见,用户感觉不到这是一个复杂的 class。

最好的例子就是 Unix 的文件读写接口,只暴露了5个方法,就囊括了所有的读写行为。

四、减少抛错

有些软件设计者喜欢抛错,一遇到问题,就抛出一个 Exception。这也导致了复杂性,用户必须面对所有的 Exception。"反正我告诉你出错了,怎么解决是你的事。"

正确的做法是,除了那些必须告诉用户的错误,其他错误尽量在软件内部处理掉,不要抛出。

Tcl 语言的最初设计是,unset() 方法用来删除已经存在的变量,如果变量不存在,该方法抛错。Ousterhout 教授说,这个设计是一个错误,完全不应该抛错,只要把 unset() 定义成让一个变量不存在,就解决问题了。

另一个例子是,Windows 系统不能删除已经打开的文件,会有错误提醒。这也是一个设计错误,有些用户实在删不掉这些文件,不得不重启系统。Unix 的做法是,总是允许用户删除文件,但是不清理内存,已经打开的文件在内存里面继续存在,因此不会干扰其他程序的运行,那些程序退出保存文件的时候,发现文件不存在才会报错。这个设计比较好。

(完)

留言(29条)

阮一峰的文章最大的特点就是真正的深入浅出,
可以将很复杂的技术用简单的文字表达出来,
确实很厉害

阮老师的文章写的很好,准备把视频搬运过来

小接口,大功能, 深深认同.

deep interface非常形象。至于simple implementation 和 simple interface的争论,其实古来有之。在 Worse is Better 这篇文章里面提到了,就是MIT/New-Jersey Approach.

在很多情况下,simpele interface over simple impl是很明显的。不过有时候这个界限也比较模糊。比如在上面那篇文章里面也可以看到,interface 和 implementation 之间的boundary会不断地变化。比如读取文件错误码EINTR你应该如何处理?是否应该暴露给user还是内核自己消化掉?

关于软件的Simplicity和Complexity,我觉得讲的最好的是clojure的作者Rich Hickey,它是真正洞悉软件复杂度本质的男人,强烈建议阮老师看下他的所有演讲,并且学下clojure,我敢保证clojure会是接下来的百年编程语言。

课程讲稿在哪儿?

阮老师 小课堂 开课了~

关于软件的Simplicity和Complexity,我觉得讲的最好的是clojure的作者Rich Hickey,它是真正洞悉软件复杂度本质的男人,强烈建议阮老师看下他的所有演讲,并且学下clojure,我敢保证clojure会是接下来的百年编程语言。

不认同减少抛错这一条,题主只是非常主观的举了一些例子,而且这些例子也并没有正确的说明应当减少抛错。实际上,抛错与不抛错,是一个软件在不同的阶段的处理方式,这个方式转化为曲线实际上是一条波浪线,如果非常主观的偏向一个方面,则表示并没有跳出当前阶段看待问题。

请问下阮老师,const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')。这种注释语法要想传变量怎么办啊?

“除了那些必须告诉用户的错误,其他错误尽量在软件内部处理掉,不要抛出。”

嗯,非常有道理!


看了阮老师的笔记,期待看原著了。

文中说的这几条基本属于老生常谈,其它几本书中都有。

160 多页的英文书籍,其实内容并没有多少!但是感觉作者写的确实好,好书不在于页数!

@surferHalo

以下是王垠对Clojure的批评:

再来说一下Clojure。当Clojure最初“横空面世”的时候,有些人热血沸腾地向我推荐。于是我看了一下它的设计者Rich Hickey做的宣传讲座视频。当时我就对他一知半解拍胸脯的本事,印象非常的深刻。Rich Hickey真的是半路出家,连个CS学位都没有。可他那种气势,仿佛其他的语言设计者什么都不懂,只有他看到了真理似的。不过也只有这样的人,才能创造出“宗教”吧?

满口热门的名词,什么lazy啊,pure啊,STM啊,号称能解决“大规模并发”的问题,…… 这就很容易让人上钩。其实他这些词儿,都是从别的语言道听途说来,却又没能深刻理解其精髓。有些“函数式语言”的特性,本来就是有问题的,却为了主义正确,为了显得高大上,抄过来。所以最后你发现这语言是挂着羊头卖狗肉,狗皮膏药一样说得头头是道,用起来怎么就那么蹩脚。

Clojure的社区,一直忙着从Scheme和Racket的项目里抄袭思想,却又想标榜是自己的发明。比如Typed Clojure,就是原封不动抄袭Typed Racket。有些一模一样的基本概念,在Scheme里面都几十年了,恁是要改个不一样的名字,免得你们发现那是Scheme先有的。甚至有人把SICP,The Little Schemer等名著里的代码,全都用Clojure改写一遍,结果完全失去了原作的简单和清晰。最后你发现,Clojure里面好的地方,全都是Scheme已经有的,Clojure里面新的特性,几乎全都有问题。我参加过一些Clojure的meetup,可是后来发现,里面竟是各种喊着大口号的小白,各种趾高气昂的民科,愚昧之至。

如果现在要做一个系统,真的宁可用Java,也不要浪费时间去折腾什么Scala或者Clojure。错误的人设计了错误的语言,拿出来浪费大家的时间。

每周看看阮一峰老师的周报,最起码我们可以跟随步伐,增长见识。

我觉得最后错误那里是 仁者见仁,智者见智了 ,其他 的都同意

如果是一个产品的话,我觉得内部处理错误的思想不错,但是如果是后台代码,我觉得所有不曾预料掉的或者环境方面的错误应该抛出,因为这样能很快定位到错误

我觉得错误是否要往上抛取决于使用方是否会关心这个错误。

谁有电子版的书啊.

“Windows 系统不能删除已经打开的文件,会有错误提醒。这也是一个设计错误,有些用户实在删不掉这些文件,不得不重启系统。”

反对,如果不在删除的时候提示无法删除打开的文件,直接不声不响的把这个文件删了,那后续对该文件的操作还是会报错的,这时候再报错很可能就已经晚了,已经造成了数据丢失。除非你从始至终都不报错,哪怕用户的数据突然无法读写了也不报错,反正用户眼不见心不烦,岂不美哉?

一个错误处理原则就是,系统无法处理的错误应该尽早抛出,而不是在掩盖错误勉强维持运行。这样才能让用户尽快定位错误所在,尽早解决问题,避免损失扩大。

至于遇到删不掉的文件就去重启系统,这只是博主你和某些人使用电脑的坏习惯,并不是一个正确的解决办法,用这一点来说明这个案例根本站不住脚。你要做的是找出是谁在占用这个文件资源,这个占用的行为是不是必要的,再来判断这个文件该不该删除,而不是重启了事。先不说很多计算机设备根本不允许随便重启,就算重启后也不一定就会释放该文件资源。

Unix 这种不声不响删掉文件的做法造成了多少rm -rf的惨剧?这和删除时报错哪个代价小?这么看来究竟是哪个系统设计错误呢?

每一次拜读,都感觉更新了自己的知识

@LDxy:

Unix 从设计上避免了运行时删除文件导致数据无法读写的情况,这是一个很好的“Define the errors out” 的设计。https://superuser.com/questions/302041/when-does-rm-remove-open-files

引用lispppppp的发言:

阮老师的文章写的很好,准备把视频搬运过来

视频在哪里呢?我觉得读阮老师的博客,进步很大。

Windows 系统不能删除已经打开的文件……
rm -rf /

引用SineTitan的发言:

Windows 系统不能删除已经打开的文件……
rm -rf /

我的意思是这种涉及到非安全操作的设计未必真的是错误设计

实际企业中,特别是时间相对长一点IT设备或者通信公司,对于软件系统方面的东西不是很看重,实际上也没法度量和管理。我说的系统方面是指System,即软件非功能性需求。除非老大重视了,这东西才能得到重视。 与领域知识Domain方面相对立。
只有纯软件公司、或者欧美部分大企业才可能同时看重系统System和领域Domain。

封装变化

微软删除文件那个可能面向的受众面不同,微软的系统要考虑到用户是电脑水平一般的大众使用,怕大家误删文件,从严谨上考虑才这样设计的。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接