Tcl 和 Raft 发明人的软件设计哲学

Posted 企鹅杏仁技术站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tcl 和 Raft 发明人的软件设计哲学相关的知识,希望对你有一定的参考价值。

作者 | 章烨明

杏仁医生CTO。中老年程序员,关注各种技术和团队管理。

Tcl 和 Raft 发明人的软件设计哲学

John Ousterhout(斯坦福大学教授,Tcl 语言、Raft 协议的发明人...真的是超级牛人,Title 好多好多,这里就列几个大家熟悉的),在 Google 做了一次演讲,题目就叫 「A Philosophy of Software Design」。看看完后很有感触,做了一些记录。

软件设计的秘密

首先,John 问了大家一个问题,什么是计算机科学里最重要的事情?下面有回答 Abstration 的,有回答 Complexities 的,有回答 Testing 的。他还问了 Donald Knuth(高德纳,程序员应该都认识吧),Kunth 说是 Layer of Abstration,而 John 的答案是 Problem Decomposition。

John 认为,很不可思议的是,在计算机科学的教育里面,没有任何课程是真正教学生软件设计的。所以他在斯坦福开设了一门课程,在这门课里,John 会教学生一些设计方法和原则,同时让学生做一个比较大的项目。然后 John 会 Review 学生的代码,上课讨论,从而让学生掌握软件设计的方法和原则。

John 列出了他的软件设计的原则,不过这个演讲里面,他主要只讲了三点:Classes should be deep、Define Errors Out of Existence 以及 Tactical vs. Strategic Programming。Classes should be deep

Tcl 和 Raft 发明人的软件设计哲学

Deep class 就是指接口非常简单,把复杂度都隐藏在实现里面的类;而相应的 Shallow class 则相反。这个原则其实不仅仅针对类,对于系统、模块、方法等其实都一样,提供简单的接口,把复杂性封装起来。

Tcl 和 Raft 发明人的软件设计哲学


Tcl 和 Raft 发明人的软件设计哲学

一个很典型的 Shallow 方法如下:

Tcl 和 Raft 发明人的软件设计哲学

这里有个小争论,听众里有人提出,有时候我们需要用一个 Shallow class 来做隔离,类似于我们常说的胶水层或者说 Facade 模式。比如说,我们现在可能用 Map 实现一个简单的缓存,那这个类必然是一个 Shallow class,但未来也许我们会换成分布式缓存。其实 John 也认同这种说法,但他说上面这个例子的方法太 Specific 了,你很难用别的方式来实现而不改变方法的语义。

然后 John 批判了一下那些「方法不能超过 20 行」的准则,滥用这些准则很容易造成很多 Shallow class。然后还批判了一下 Java 的 IO 实现。

Tcl 和 Raft 发明人的软件设计哲学

我以前用 Java 也一直觉得听吐血的,在 Scala、Python 里面文件读写都是一行就能处理的事情,但在 Java 里要用好多 Stream 类要写好多行代码。文件读写是软件开发最常用的操作,而对于最常用的操作,应该提供最简单的接口。比如这个文件读写,默认就应该提供 Buffering,但可以有个选项说我不需要 Buffering。

那什么是好的 Deep class 设计呢?John 的例子是 Unix 文件 IO,五个方法,简单明了,但后面隐藏了巨大的复杂性。不管是什么文件系统、什么设备,只要实现了这五个接口,就能和普通文件一样读写。


Tcl 和 Raft 发明人的软件设计哲学

很多年前我写 Linux 程序的时候,也很震惊于这一点,于是去读了 Linux 的源代码。我发现它用 C 语言的指针,很优雅的实现了类似面向对象的多态。其实 Unix 和 Linux 里还有很多类似的优雅的设计,比如管道,「Unix 编程艺术」里有不少介绍。

Define Errors Out of Existence

Tcl 和 Raft 发明人的软件设计哲学

一个方法如果抛出太多的异常(或者错误),其实是对调用者的负担。更好的方式是,重新定义方法的语义,从而避免异常。

John 举了几个例子。一个例子是 Tcl 语言里的 Unset 操作,用来删除一个变量,当这个变量不存在的时候它会报错。其实我们也常常这样处理 CRUD 里的删除操作,如果发现对象不存在,就抛一个异常。调用方就必须检查这个异常,而处理方式常常就是忽略它。然而我们可以把删除操作的语义重新定义一下,我们可以定义删除就是确保某个事物不再存在,那么如果它本来就不存在,我们什么都不用做就是了。

另外一个例子是 Java String 里的 substring 操作,这个相信很多 Java 开发者都有感受。

public String substring(int beginIndex, int endIndex)

如果传入的参数越界,或者 beginIndex 大于 endIndex,它都会抛异常,我们不得不在传入之前检查一下。其实更好的方式是,substring 返回该字符串和参数指定范围的交集就好了,如果越界或者 beginIndex 大于 endIndex,就返回空字符串。Apache Common 的 StringUtils 就是这么设计的。

这里有位听众也提出了很有意思的争论,他说调用方可能会误用,开发者应该想办法避免被误用。John 的回复是,你很难避免人们误用,而我们有时会为了避免误用,反而会引入很多不必要的复杂性,不管是对调用方还是对实现方,这是得不偿失的。我们设计软件或模块时,应该要让常见场景和正确调用都很简单。

不过要注意,John 的本意并不是说完全不要用异常或不要做错误处理,必须处理的错误还是要处理的。

Tactical vs. Strategic Programming

这里 John 提出了一个战术编程(Tactical Programming)和战略编程(Strategic Programming)的概念。战术编程就是没有太多设计,简单粗暴快速实现;战略编程则是需要做良好的设计,为长远考虑。

Tcl 和 Raft 发明人的软件设计哲学

Tcl 和 Raft 发明人的软件设计哲学

如果用战术编程的方法,一开始的确会快一些,但是 John 认为,大约 6~12 个月后,战术编程的负面效果会很明显的展现出来,严重降低开发速度和质量。顺便我也提一句,如果是创业公司的话,战术编程的确是一个选项,毕竟 6~12 个月后你的公司是不是还活着都是一个问题;但如果一个创业公司熬过了这段时间,还选择一直战术编程的话,会很危险。

Tcl 和 Raft 发明人的软件设计哲学

然后 John 吐槽 Facebook 当年的「Move Fast and Break Things」,说 Facebook 这两样都做到了,他们的代码是一坨屎(下面 Google 的人很开心)。然后他说他有两个学生当年去 Facebook 实习,然后被吓坏了。这两个学生之前一直在抱怨为什么要写单元测试、要写文档,然后从 Facebook 回来后他们就再也没抱怨过。后来 Facebook 把这句改成了「Move Fast With Stable Infrastructure」。最后又表扬了一下 Google(下面 Google 的人又很开心)。

其实在杏仁,我们也要求开发在实现复杂需求前,要先写 Tech Design,想清楚再开始编码,也是希望大家都能进行战略编程。然后我们的团队文化,也有一句是「童子军原则」,也是这个道理。当然,话虽如此,要做到 John 说的 Great Design 还是很难的。不过,即使做不到 Great 至少也要做到 Good Enugh,而这也许并不会要花太多时间,只是需要良好的架构和编程习惯。

总结

John 其实把整个话题都写成了一本书,名字也是「A Philosophy of Software Design」。目前国内只有英文版,但亚马逊和当当都卖断货了!等有货了我一定要买一本,也希望尽快有中文版。

最后,我也想提醒一下,这里提到的原则,其实也都是一个 Overall 的原则,也就是说这些原则如果和其他设计方法或原则有冲突,是需要去权衡利弊的。用 John 的话说,这就是软件设计的「哲学」。


全文完



以下文章您可能也会感兴趣:



我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。



杏仁技术站



以上是关于Tcl 和 Raft 发明人的软件设计哲学的主要内容,如果未能解决你的问题,请参考以下文章

[转] Raft 实现日志复制同步

软件设计的哲学: 第十章 定义不存在错误

2020荐书:软件设计的哲学

软件设计的哲学:前言

软件设计的哲学:第十一章 两次设计

处世哲学