clean architecture(上)

这半年来和同事陆陆续续有一些关于业务的代码、框架方面的争论,期间阅读过一篇陶师傅给的,推销 DCI 模式的文章/论文,认真地做了笔记。年底了,又有美团的人跳出来说互联网的业务也越来越复杂了,传统的糙快猛已经满足不了当下的需求,我们必须要认真考虑设计,而不是在学到了样子没学到根本的敏捷开发思想之上堆屎。看起来现在有不少人把矛头指向了曾经奉为圭臬的 OO 和 web 开发中最常见的 MVCL(S) 分层以及国人学了半瓶水的敏捷开发,并且各自提出了一些所谓的解决方案,比如 DCI 号称解决了 OO 驱动的开发模式下代码难以和需求所对应,而 DDD 也解决了项目上了规模以后,代码和业务描述脱节难以理解的问题。(这么看来两者解决的都是一样的问题。

虽然通过阅读,我对这些人的观点都算是有了粗浅的了解,但还是没有办法能够将这些观点宏观地串在一起,做到融会贯通。

直到年底读完了 Uncle Bob 的新书 Clean Architecture。这本书是 2017 年 10 月出版,所以知识的新鲜度还是可以的。本来难得地想通过美亚来买到,结果在拿到书之前已经把电子版都读完了(汗。废话少说,开始正题。

这篇文章差不多就相当于是读书笔记了。因为内容比较多,所以分上下篇吧。如果我懒了可能也就没下篇了233。

设计与架构

设计 (design) 和架构 (architecture) 在软件工程上虽然是两个词,不过其实本身说的是一回事。为什么要设计?或者软件为什么要有架构?本质是为了能够最小化创建和维护软件的成本。这么说比较虚,来看看一个比较传统的软件公司的发展趋势,第一是随着软件生命周期变化,工程师的数量变化:

然后还是随着产品生命周期的推进,整个软件的代码行数变化:

工程师越来越多,但代码的增长速度越来越慢,那下面这张图的结论也就显而易见了:

每一行代码的成本都变高了。如果说你的老板看到程序员每写一行代码需要花掉 400 美金的时候,你觉得他会怎么想?

当然我这么说你可能会反驳,那是老板的事,我写我的代码,管他娘的。现在我写一行代码就能赚 400 美金,那我一个月的工资也就只要写个几十行就搞定了,开心啊~

想得美,如果你费了吃奶的劲一个月也只能痛苦地写出 20 行代码,这 20 行代码说不定还影响巨大,牵一发动全身,不知道就触发了哪里的其它人写的 bug。这说明系统本身出了问题。问题就是出在了设计/架构上。这样的情况对于公司来讲是不可持续的,随着业务继续发展,未来等待这家公司的结果也只能是万丈高楼轰然倒。

在说系统的设计/架构问题之前,先来看看程序员中流行比较广的花言巧语。

写乱七八糟的代码能让项目走得更快。

有问题的代码完了我会改的,现在要紧的是先占领市场(作老板状

tdd 这种东西是上个世纪的遗物,现在谁还用这玩艺儿。

既然说这些是花言巧语,那自然都是工程师们的谎言。这里甚至可以直接拿小学一年级的龟兔赛跑来打比方,兔子的过度自信让它输掉了比赛。而大多数工程师也和当初那只愚蠢的兔子没什么区别,一样对自己的生产力拥有迷之自信。最终在堆了几个月的屎之后发现自己的代码再也没有办法去增加新的逻辑或者特性。(当然,还有聪明的人想出了所有的逻辑都可以加在原来代码最后这种天才想法。经手的项目全变成意大利面条式的代码。这些想法让历史项目变成了一个公司的历史包袱,而不是技术积累。

可能工程师这个时间点会提出重构可以解决问题。对,重构是可以解决一部分问题。问题在于这些负责重构的程序员思想可能和当初写历史包袱的那些程序员没什么两样:

这里代码什么意思,看不懂,总之就先抄过来。

我艹,这里的逻辑怎么是这样的,好像没办法满足我的新设计,还是按着原来这个 if 来写吧。

新的需求怎么和原来的流程完全不一致啊,那我先把它写在这个函数的最后面,之后再改。

工程师的过度自信让他们和当初那只兔子一样,重构后的项目和原来一样屎(或者就稍微好了那么一点。

最后是 TDD,大多数人(包括我)都认为 TDD 会让项目开发变慢,甚至有些激进的工程师因为不知道在哪里被人蛊惑,直接就认为 TDD 这种东西就是邪道。那么我们看看 Jason Gorman 做的一个实验,实验总共花 6 天时间,每天都写一个简单的把阿拉伯数字转换成罗马数字的程序,在使用 TDD 的日子里,只要 test 通过,那么就可以认为程序正确,不使用 TDD 的日子里需要用另外的方式来验证程序正确。然后对花费的总时间进行统计:

结果令人大跌眼镜对不对。因为程序都是一致的,所以无论使用不使用 TDD,之后所花的时间都会越来越短,但在这六天实验中,使用 TDD 花费的最长的时间也比不使用 TDD 花费的最短的时间也短。所以这导出了一个比较重要的结论:

The only way to go fast, is to go well

对于公司来说,要保证系统架构设计合理。对于工程师来说,需要保证自己深刻地清楚什么样的架构是好的架构。这里的架构是广义的。即使模块内部的职能划分也算是架构的一环。

软件的价值

对于商业公司的金主来说,软件系统有两方面的价值,一方面是软件的行为价值,也就是指软件的业务功能;另一方面是软件的结构,指软件的架构,易变性,可维护性,属于软性价值。软件工程师的职责是保证系统两方面的价值都能够达到最大。但是实际情况是,大多数人就只会聚焦在某一个方面,顾此失彼。

举个例子,业务工程师完成 pm 的需求,把需求文档翻译成代码,让机器能够满足需求中提到的所有行为。很多工程师觉得这就是他工作的全部内容了。当然了,这种想法是错误的。

代码组合成的是软件,软件的英文单词是 software,soft 本身要求系统能通过轻松地修改代码,来改变机器的行为。而不是直接改变机器的行为,如果是想改变机器行为本身的话,那就是 hardware 的事情了。如果金主想要增加或者修改软件系统的 feature,这种修改应该是很容易做的才对。这要求我们的修改就真的只是修改,而不是把整个系统的样子全都变了。

对于工程师来说,老板提的需求就像是一连串的谜题,他们需要解开这些谜题,然后在自己的系统里去支持。而这些谜题的难度随着产品的发展,变得越来越难。可能某一天就真的非常难以实现了。

这时候就要思考是不是架构出问题了。

上面提到了软件的两种价值是:行为价值和结构/架构价值。如果让工程师或者老板二选一的话,你觉得哪种价值更为重要?比如现在给你两个系统,实现的功能差不多,第一个系统已经可以跑了,但是代码一坨屎,几乎没有办法修改;而另一个系统跑不起来,但是因为设计很优秀,所以修改起来很容易。你会选择哪一个?

大多数人可能会选择那个马上就能跑起来的系统。这个观点还体现在很多老板的言辞中:功能要紧,灵活性不重要。但如果之后需求变更的时候原来的代码完全没有办法修改了,狂怒的也是老板。

在上面例子的前提下,修改系统所能得到的收益可能都已经超过了修改系统所需要的成本了。因为很多时候,经理或者老板没有办法理解架构的重要性,保证架构的合理性工作就落到了工程师的头上,这也正是一个软件工程师之所以被受雇的原因。尽管在开发过程中,所有架构都在不断经受挑战,而工程师则需要去面对这些挑战,还是 Uncle Bob 说的好,在开发和设计过程中工程师始终都在 “struggle”,以使软件两方面的价值都能够达到最大。

编程范式

业界发展多年,一直到现在被接受的可称为编程范式的没有几个:结构化编程范式、面向对象编程范式、函数式编程范式,这三种范式被大众接受并有相对较为广泛的应用。

结构化编程

结构化编程,指的是所有程序都可以用:
sequence/selection/iteration 来表达。这个其实比较好理解,sequence 就是从上往下(误)写代码;selection 最常见的就是 if/switch/go 的 select,根据条件选择分支,然后执行;iteration 则是指 foreach x in x_list 或者 for i = x.begin();i!= x.end(); i++ 这样的叠代。

使用这些逻辑来表达的程序可以按照功能进行划分为一个一个的子函数/过程,然后函数再聚合到一起,形成模块(module)。

但早期的编程语言有一个麻烦的问题,会导致结构化的划分方式失效,这个恶魔就是 goto。goto 本质上就是一条汇编无条件跳转指令 jmp(或者衍生品)。而只要干掉 goto,程序本身就可以“递归”地划分为一个一个的函数、包含函数的模块、包含模块的 lib。

上面这些观点最早是 Dijkstra 提出的,你没看错,就是这个当年在图论或者离散数学或者算法导论里折磨过你的男人。

虽然现代的编程语言有不少语言还保留了 goto 这个关键字,但都将其跳转范围限制在了函数内(比如 golang),这样既可以灵活的控制函数的逻辑、统一处理错误清理资源,也不会破坏结构化程序的模块划分。

这些显而易见的结论落实下来,当年那也是一波“腥风血雨”,或者浪漫一点,是一介奶油小生挑战业界巨匠的名场面?

“In 1968, Dijkstra wrote a letter to the editor of CACM, which was published in the March issue. The title of this letter was “Go To Statement Considered Harmful.” The article outlined his position on the three control structures.

And the programming world caught fire. Back then we didn’t have an Internet, so people couldn’t post nasty memes of Dijkstra, and they couldn’t flame him online. But they could, and they did, write letters to the editors of many published journals.

Those letters weren’t necessarily all polite. Some were intensely negative; others voiced strong support for his position. And so the battle was joined, ultimately to last about a decade.

Eventually the argument petered out. The reason was simple: Dijkstra had won. As computer languages evolved, the goto statement moved ever rearward, until it all but disappeared. Most modern languages do not have a goto statement—and, of course, LISP never did.”

摘录来自: Robert C. Martin. “Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)”。 iBooks. 

正确的理论一旦挑战到人们的常识,一般人的第一反应大概都会强烈抵触吧。只能让时间去慢慢证明了。

不过再想想 Dijkstra 如果活在这样的网络时代,不知会不会像 linus 那样来一句 read the f**king discrete mathematics textbook!(逃

工业界在承认了 goto 的危害性并且广泛避免开始接纳结构化编程范式之后,就可以把程序分解成一个一个可证明的模块和函数了。

“可证明”本来是数学上的理念,但软件系统本身和数学并不一样。程序更像是科学范畴的事情。科学的可证明显然和数学的可证明不是一回事。数学的证明一般是通过定理和推论来证明理论的正确性。而科学的可证明更多时候是在证伪,如果没有手段对某个理论进行证伪,那么一般就认为这个理论是正确的。所以实际上科学和软件都是“不可证真”的。

具体到计算机软件,想想你用的证伪手段是什么呢?对的,就是 tests。想想你为什么要写 test,如果 test 跑过了也只能证明当前系统在这个 case 下没有问题,但并不能保证系统的运作是绝对正确的。

面向对象编程

面向对象诞生的起因是 Dahl 和 Nygaard 将函数的调用栈移动到了程序的堆区,然后发明了 OO。OO 一般通过 o.f 这种形式调用具体的函数。而 OO 的定义,是“将数据和函数进行组合”。当然也有人会说 OO 是一种描述和建模真实世界的技术。时至今日,人们用三个单词来概括 OO 的特点:

encapsulation 封装

inheritance 继承

polymorphism 多态

最早有封装概念的是 C 语言,把头文件提供给别人用,所有在头文件中声明或定义了的变量、数据结构、函数都可以为外人所用。如果在具体的实现中有一些内容不希望别人关注,那么就把这些东西定义在 .c 文件中。比如:

point.h:
struct Point;
struct Point * makePoint(double, double);

point.c
#include "point.h"
#include <stdlib.h>
// .c 中定义的结构体,在 .h 中无法访问,且对外部不可见
struct Point {
    double x;
    double y;
};

struct Point * makePoint(double x, double y) {
    struct Point * p = malloc(sizeof(struct Point));
    p->x = x;
    p->y = y;
    return p;
}

通过声明和实现分离,做到了完美的封装。只让调用方看到他所关心的部分,而不暴露任何内部的实现细节。

后来紧接着出现的语言是 C艹,C艹中引入了 public 和 private 关键字,这相对于原来的 C 实际上并不是一种进化,而是退化。C艹对外提供服务和 C 同样使用头文件,在该前提下调用方就可以看到你的 private 变量,这些本不是调用方应该关注的内容。实际上 C艹当初这么做,也确实是因为技术原因。因为 C艹的编译器需要知道每一个 class instance 的大小,所以需要你在 header 中把内部变量都声明好。这实际上只是一种 hack。

再到后来的新语言,比如 Java 或者 C#,连类的声明和定义都不进行分离了。这对于“封装”本身实际上又是再一次退化。相比最早的 C 语言,后续的“OO 语言”的封装都没有当初那么完美。

然后是继承。C 语言中已经有了类似继承的雏形技术。其本质其实是类型强制转换。参考下面的代码:

namedPoint.h
struct NamedPoint;
 
struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);

namedPoint.c
#include "namedPoint.h"
#include <stdlib.h>
 
struct NamedPoint {
  double x,y;
  char* name;
};
 
struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
  struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
  p->x = x;
  p->y = y;
  p->name = name;
  return p;
}
 
void setName(struct NamedPoint* np, char* name) {
  np->name = name;
}
 
char* getName(struct NamedPoint* np) {
  return np->name;
}

main.c
#include "point.h"
#include "namedPoint.h"
#include <stdio.h>
 
int main(int ac, char** av) {
  struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
  struct NamedPoint* upperRight = makeNamedPoint (1.0, 1.0, "upperRight");
  printf("distance=%f\n",
    distance(
             (struct Point*) origin, 
             (struct Point*) upperRight));
}

这种“继承”要求的是“父类型”和“子类型”的前几个字段排列顺序是一致的。C艹的单继承也是这么实现的。

所以在 OO 语言出现之前,就已经有了这样的套路。而继承也不能算是 OO 语言的原创。只是在 Java 之类的语言中我们使用继承更加方便一些了而已。

当然了,野路子的继承肯定还是和正路的不一样,没有办法实现多继承(现在也没人用或者新语言不支持了)或者字段顺序不一样的正统继承。

OO 最宝贵的新概念可以说是多态。没有多态之前,C 程序员想做适配怎么做呢?使用函数指针。类似最早 Unix 对于 IO device 的做法,定义一套 IO 函数,包括 open/close/read/write/seek,然后所有的 IO 设备只要支持这些接口,就可以在不修改业务代码的情况下完成 IO。但函数指针毕竟是函数指针,并不是安全可靠的做法。在 OO 里,使用多态有更为普适的意义,例如下面的例子:

回忆一下结构化编程里,我们把功能划分为函数,组合函数形成模块,然后再把模块组合起来形成软件。如果我们的模块依赖于另外的模块,那么我们就需要把这个依赖方 #include 、import 、using 进来,这样的结果是我们的程序依赖方向和控制流的方向是完全一致的。图中的绿线是控制流方向,而红线则是依赖方向。我们并没有什么好办法来改变这个现状,只能使用起来比较“危险”的不知道什么时候可能会 coredump 的函数指针来做一些依赖抽象方面的事情。

而 OO 的多态可以让我们做什么样的事情呢?


我们在自己的模块中可以声明一个接口,然后让外部的模块来实现这个接口。但我们通过接口来调用外部模块的功能,所以控制流的方向是:

自己的模块 -> 外部模块

而依赖方向:

外部模块 -> 自己的模块

通过接口,我们实现了依赖反转。这也就是多态带给我们最有价值的东西。

在业务类的系统中,我们通过多态可以实现类似下图效果:

让所有模块的依赖方向(红线)全部指向业务规则。

函数式编程

传统编程语言中的变量是可以改变的(废话。也就是说 var xxx 一会可以是 1,一会可以是 2,在有的语言里可能一会儿会变成 "oh no"。因为可变,所以才叫变量。而一些声称函数式的编程语言中最重要的概念可能不是什么 lambda 表达式,也不是函数链式调用,而是变量本身的不可变性(immutability)。

你可能暂时想不明白为什么不可变性是函数编程最重要的原则,先来回想一下下面的几个问题:

为什么多线程/多核心编程中会有 race condition

为什么对数据加锁有些情况下会 deadlock

为什么数据并发写会有问题

没错,这些问题的直接原因就是因为“变量是可变的”这么一个简单的道理。当变量不再可变的时候,上面这些问题也就不复存在了。

当然了,函数式语言不能解决所有问题。在函数式语言火爆的时间点,有很多架构师会经常讲 event sourcing 的概念。event sourcing 是个什么东西?

简单来讲,对于一条数据的操作,我们记录初始值,和所有的操作事务,就像这样:

initial happiness: 1
trans: +1
trans: -2
trans: -5
trans: +100

只存储事务,而不存储状态,如果我们需要获取某个值的当前状态,那么只要在初始状态上把所有的事务叠加计算即可。你可能觉得稍微有点眼熟,对啊,想想你正在用的版本控制系统不就是这么做的么(笑?

event sourcing 和函数式编程对于资源有一个简单的假设,我们计算机的计算能力和存储能力是无穷的;哪怕现在不是无穷的,因为有摩尔定律在所以未来也有可能是无穷的。这样我们可以通过事件序列精确地对事件进行溯源,并得知一个变量所有的历史状态。

这只是个美好的愿景,显然计算机的算力就不是无穷的,硬盘也不是无限大的。如果你手边有计算类型的系统的话,基本都可以看到一种折衷,每天进行一次计算,然后后一天以前一天的静态结果和当天发生的所有事件再进行一次计算。对,就是 T + 1 的离线统计类计算。

设计原则 SOLID

前面说到软件系统需要设计,但设计是从微观一直到宏观的,比如函数和类怎么设计,模块怎么设计,整体的架构怎么设计。这些设计需要遵循一定的原则。从理论上来讲,任何函数都可以放在任何类中,也可以放在任何文件中,也可以放在完全不相干的模块中,但如果你真的这么做了,那所谓的设计也是扯淡。

SOLID 就是微观设计的五个原则。相较模块、架构这种较宏观的层面,SOLID 主要针对较微观层面的设计,例如函数、类应该怎么设计,接口应该怎么设计。

SRP

基本是个工程师都知道 Single responsibility principle,并且自以为是地在自己的函数中、类中遵从这个原则来设计和编码。不过 SRP 的具体含义是啥呢?
相信 99% 的人都会给出这样的回答:

一个函数/类只干一件事。

当然了,这种看法也没什么问题。实际上 SRP 的含义是:

A module should have one, and only one reason to change.

为了精确,这里还是用英文原文了。是不是比较反直觉。实际上 SRP 对程序的要求是:如果一个函数有多个用户,那最好不要公用它。实际上 SRP 和程序员们坚持了多年的 DRY 原则某种程度上是相悖的。大多数人会在系统的初期,把那些看起来很相似的代码放在一起。

举个例子,在某个业务系统的 class 中,有一个 reportHour 的函数,本来是用来给 CTO 团队的人工程师工作时长的,在第一版的需求中只计算工会规定的 8 小时工作制中的时间,加班时间并不会计算在内。而某个 CFO 团队的倒霉蛋在同一个类里写 calculatePay 函数,这个函数需要有一个计算员工规定正常工作时间内的时间(不包括加班哦,你看加班是没有工资发的。另外这个公司发的大概是时薪)。这个倒霉蛋发现这个类里碰巧有这么一个 reportHour 函数,然后就欢欢喜喜地用别人写好的函数完成了自己的功能。结果有一个,CTO 团队表示我们评绩效的时候需要考虑到工程师的加班时间,需要修改 reportHour 函数,所以把加班时间也计算上。然后 CTO 团队的人欢欢喜喜地把 reportHour 修改了。结果自然是可想而知,公司当月的支出大额增加。老板大为光火。

错在谁呢?错在当初本来没有任何关系的两个团队,使用了同一个类,而发生了不应该的代码耦合。正确的做法是从一开始就把双方所使用的 class 分开,CFO 团队去完成自己的 reportHour 函数。

实际开发中有很多类似的事情,有一些看起来相同的需求最初看起来相同。但可能叠代一次就完全不一样了。而让这些需求去公用代码,本身也是一件危险的事情。A module should have one, and only one reason to change。这里的 one reason,实际上指的是一个外部角色的需求。电商系统里我们可以认为顾客是一种角色,而商家是一种角色。打车系统可以认为司机是一种角色,乘客是一种角色。再细分到某个子系统,可能实时类型的订单算是一个角色,而预约类型的算是另一种角色。

不要机械地遵循 DRY 原则。

OCP

OCP = Open-Closed Principle。

A software artifact should be open for extension but closed for modification.

是说软件的行为最好能够在增加代码,而不是修改原有代码的前提下进行修改。这条原则也有很多工程师有误读,认为 OCP 应该通过继承来实现,每次要修改功能,我们就去写新的子类,去覆盖父类的方法。

后来他们就写出了十几层或者几十层的继承树。

OCP 实际上强调的是外部的变动不应该影响类的内部,这一点要怎么做呢?其实就是使用 interface。让所有外部模块使用这些几乎不会变的 stable 的 interface。

这张图可以看得比较清楚,图中矩形右上角的 I 表示 Interface,DS 表示 Data Structure。如果我们想要让一个模块不被另一个模块的变化所影响,那么就在这个模块中定义接口。并让另一个模块的依赖方向指向不想变动的模块。这样就达到了对模块进行修改“保护”的目的。

这才是 OCP 的本质。OCP 还要求我们尽量避免传递依赖,不要依赖我们没有直接用到的类和接口。这一点在后面的 ISP 会有更详细的说明。

LSP

LSP = Liskov substitution principle。李氏替换原则。其最原始的定义是:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T

考虑到有人一读英文就头痛,简单翻译一下:
对于 S 类型的对象 o1,如果存在 T 类型的 o2,在程序 P 中把所有 o2 替换为 o1,整个程序的行为没有任何变化,那么就认为 S 是 T 的子类型。

举个例子:

这个很好懂,你的 Personal License 和 Business License 都应该含有基本 License 定义中的一些成员、函数、组件等等。没什么可说的,License 本身都有一定的相似性。

还有一个经典的没有办法使用 ISP 的例子:

被称为矩形正方形难题。正方形按理说应该是矩形的子类,但如果真的这么做了,父类中的 setH 和 setW 必然会导致子类的长=宽的约束被破坏。但你却没有办法限制用户不去使用父类的方法。最终可能导致完全的逻辑错误。

所以也不是所有的问题都可以用 LSP 来进行总结。

刚开始的时候,人们以 LSP 来指导类的继承设计。但近些年来,也一样变成了 Interface 和 Implementation 的问题。

ISP

ISP = Interface segregation principle,接口分离原则,是关于如何设计接口的。比较简单,看两幅图就懂了。

不太好的接口设计:

优秀的接口设计:

也是“不依赖你不需要的东西”的一种衍生原则。

DIP

DIP = Dependency inversion principle。依赖反转原则。DIP 教导我们最灵活的系统内部,代码都依赖抽象类或者接口,而不是具体实现。

遵循这条原则要做到:

Don't refer to volatile concrete classes.

Don't derive from volatile concrete classes.

Don't override concrete functions.

Never mention the name of anything concrete and volatile.

几句指导的意思其实差不多。。Java 里常见的抽象工厂模式就是这种指导下的一种实践。

同样,保证了依赖的方向是从具体模块指向抽象模块的。这里和前面多态中提到的一样,通过依赖 Interface 而不是具体实现,我们将依赖的方向调整为与控制流完全相反的方向。

这种手段使架构师有了在系统内部任意位置调整依赖方向的能力。

简单总结一下 SOLID 原则:

1.模块应该按照模块所服务的 actor 来划分

2.使用 interface 保护不想受外部变化而变化的模块

3.如果要对外部模块进行替换,让新的模块实现原来的 interface 即可

4.interface 的粒度要尽可能细

5.通过插入 interface,可以在任意两个模块之间反转依赖关系

你看,也没那么玄乎对不对。说白了就是合理使用 interface。为什么这里没提抽象类呢?

实际上大多数情况下根本不需要抽象类,只要 interface 就可以解决大部分的问题,高斯林在谈到 java 设计的时候也讲过其他很后悔没有把 java 做成一门纯 interface 的语言:

I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied. After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible.

这一段出自这里

现在经常会听到有人吐槽 golang 这样的新语言不支持传统的面向对象。槽点主要集中在继承上。

不过你仔细想想,面向对象中的继承真的那么不可缺失吗?

待下篇续~

====== 这里是华丽的分割线

妈妈说文章要写得长看起来才像读书人。

Xargin

Xargin

If you don't keep moving, you'll quickly fall behind
Beijing