This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<audio id="audio" title="04 | 三步走:如何了解一个软件的设计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/35/a9/354365bd7e0c0d056867ef72d02a87a9.mp3"></audio>
你好!我是郑晔。
经过了前面几讲的铺垫,我们已经对软件设计是什么,以及要考虑哪些因素有了一个初步的了解。热身之后,就该开启正式的旅程了。
作为一个程序员,我们在职业生涯中免不了要接手新项目,承担维护该项目的职责。如果一个新项目摆在面前,你会怎么去研究它呢?
很多人的第一反应就是去看源代码。但是,一头扎入代码中,很快你就会迷失其中,最初那股子探索精神,也会逐渐被迷茫所代替。回想一下,有多少次你满怀希望地打开一个开源项目,结果多半都是坚持不了多久就放弃了。你有没有想过,问题出在哪里呢?
你的迷茫在于缺少对这个软件整体的了解,这就如同不带地图指南针闯入密林一般,迷路只是早晚的事。所以,虽然阅读源码是必经的一步,却不应该是你的第一步。我们应该先从了解软件的设计开始。那我们该如何了解一个软件的设计呢?
## 模型、接口和实现
了解一个软件的设计可以从三个部分着手:**模型、接口和实现**。这三者的关系就好比你去看代码,你会先去看有哪些类以及它们之间的关系,这就是看模型;然后你会打开一个具体的类,看它提供了哪些方法,这就相当于看接口;最后,你再来打开一个具体的方法,去看它的代码是怎么写的,这就是看实现。
好,接下来,我们具体地分析一下每一个部分。
首先是**模型**,它是一个软件的核心部分。在其它的材料里,也有人称之为抽象,为了统一,我这里就都叫模型了。我们在前面的课程里也说过,设计最关键的就是构建出模型。而理解一个设计中的模型,可以帮助我们建立起对这个软件整体的认知。
比如你在编写分布式计算代码时需要考虑怎样在不同的节点上调度计算而使用MapReduce时只要考虑如何把计算分开Map最后再汇总Reduce而到了Spark注意力就集中在要做怎样的计算上。它们在解决同样的问题只是抽象层次逐步提高了越来越接近要解决的问题越来越少地考虑计算在不同的机器上是如何执行的由此降低了理解的门槛。
当你知道了模型的重要性,目光甚至可以不局限在某一个软件上。如果把同一个领域不同阶段的多个模型联系起来看,你还能看到软件发展的趋势。
其次是**接口**,它决定了软件通过怎样的方式,将模型提供的能力暴露出去。它是我们与这个软件交互的入口。如何理解这句话呢?我给你举几个具体的例子。
- 一个程序库的接口就是它的API但对于同样的模型每个人会设计出不同的API而不同的API有着不同的表达能力。比如Google的Guava对JDK的一些API重新做了封装其目的就是简化开发而很多优秀的做法后来又被JDK学了回去。
- 一个工具软件一般会提供命令行接口比如每个程序员必备的基本技能——Unix命令行工具就是典型的命令行接口。
- 一个业务系统的接口就是对外暴露的各种接口比如它提供的各种REST API也可能是提供了RPC给其它系统的调用。
- ……
如果你想深入源码,去了解一个软件,接口是一个很好的指向标。你可以从一个接口进入到软件中,看它是怎样完成各种基本功能的。
最后是**实现**,就是指软件提供的模型和接口在内部是如何实现的,这是软件能力得以发挥的根基。这么说可能比较抽象,我再来举些例子。
- 一个业务系统收到一个请求之后,是把信息写到数据库,还是转发给另外的系统。
- 一个算法的实现,是选择调用与别人已有的程序库,还是需要自己实现一个特定的算法。
- 一个系统中的功能,哪些应该做成分布式的,哪些应该由一个中央节点统一处理。
- 一段业务处理,是应该做成单线程,还是多线程的。
- 当资源有竞争,是每个节点自己处理,还是交由一个中间件统一处理。
- 不同系统之间的连接,该采用哪种协议,是自己实现,还是找一个中间件。
- ……
讲到这,相信你一定发现了,“实现”里面的内容很多。所以,做每一个技术决策都应该结合自己所开发应用的特点,并不存在一个通用的解决方案。在实际的工作中,我发现许多人以为的设计其实是这里所讲的实现。
我也知道,“实现”很重要,但是,它必须建立在模型和接口的基础之上。因为在一个系统的设计中,模型是最核心的部分。如果模型变了,这个软件便不再是这个软件了,而接口通常反映的就是模型。所以,模型和接口的稳定度都要比实现高,实现则是要随着软件发展而不断调整。
举个例子很多人都知道Redis这个键值对存储性能非常好他们学习Redis时对其单线程模型印象深刻因为它简单高效。但随着人们使用Redis的增多对Redis有了进一步的需求。所以从6.0开始它开始支持多线程版本以便于更好地满足人们的需求。但即便Redis改成了多线程它还是那个Redis它的模型和接口还是一如既往只是实现变了而已。
## 了解设计三步走
之所以要把模型、接口和实现区分开来,是因为这三者的关注点是不同的,而很多人在讨论所谓的“设计”时,经常会把它们混在一起。
如果你在讨论的时候连“讨论的内容到底是什么”都没弄清楚,就很难得出一个清晰的结果。我参与过很多类似的讨论,经常有一种很混乱的感觉。我思考了很长时间才发现,问题就在于他们把不同层面的内容混在了一起。
所以正确的做法是什么呢?就是你在讨论设计时应该遵循一个顺序,**先模型,再接口,最后是实现**,同理,了解一个设计也应该遵循这样的顺序。
<img src="https://static001.geekbang.org/resource/image/b1/84/b1279d9a81d8b271c01270d3da8f0684.jpg" alt="">
如果模型都还没有弄清楚,就贸然进入细节的讨论,你很难分清哪些东西是核心,是必须保留的,哪些东西是可以替换的。如果你清楚了解了模型,也就知道哪些内容在系统中是广泛适用的,哪些内容必须要隔离。简单地说,分清模型会帮助你限制实现的使用范围。
下面是一张简化过的架构图在这幅图里订单模块完成处理之后通过一个Kafka队列把消息发给支付模块支付模块处理之后再通过一个Kafka队列把消息发给物流模块。很多人都应该在自己的项目中见过类似的但是更复杂的架构图。你能看出这张图的问题在哪吗
<img src="https://static001.geekbang.org/resource/image/eb/3e/ebc3bf3cb03421de4b2a0f642940bd3e.jpg" alt="">
这张架构图的问题就在于它把模型和实现混淆在一起了。图中的订单、支付和物流说的都是模型层面的东西但Kafka的出现就把实现层面的东西拉了进来。Kafka只是实现这个功能时的一个技术选型这也就意味着如果随着业务的发展它不能很好地扮演它的角色你就可以替换掉它而整个设计是不用变的。
所以实现这段代码的时候必须把Kafka相关的代码进行封装不能在系统各处随意地调用因为它属于实现是可能被替换的。
我还要强调一点,在了解设计时,要按层次去了解,因为设计常常是分层的。每当我们打开一个层次,需要了解它的内部时,我们还要按照模型、接口和实现的顺序解读这个层次。
我用大家比较熟悉的操作系统来举个例子,如果你去了解它的内部,就知道它有内存管理、进程调度、文件系统等模块。我们可以按照模型、接口和实现去理解每个模块,就以进程管理为例:
- 进程管理的核心模型就包括进程模型和调度算法;
- 它的接口就包括,进程的创建、销毁以及调度算法的触发等;
- 不同调度算法就是一个个具体的实现。
操作系统课程难以学习,很大程度上就在于,很多人没有搞清楚其中各个概念之间的关系。
即便层层展开到最后到了一个具体类甚至是一个具体的数据结构我们依然可以按照模型、接口和实现这个结构来理解比如很多Java面试题常问到的HashMap
- 它的模型就是我们在数据结构中学习的HashMap
- 它定义了一些接口比如get、put等
- 它的实现原来是用标准的HashMap实现后来则借鉴了红黑树。
实际上,当你能够一层一层地去理解设计,就像一棵知识树逐渐展开一样,每一个知识节点在展开的时候,都会有下面一级更具体的内容。当你的头脑中有了这样一棵设计树,你也就掌握了整个系统的地图,再有新需求到来时,你就不会再盲目地去改代码了。
## 总结时刻
今天,我们学习了如何了解一个软件设计,可以从三个部分入手:模型、接口和实现。
- 模型,也可以称为抽象,是一个软件的核心部分,是这个系统与其它系统有所区别的关键,是我们理解整个软件设计最核心的部分。
- 接口,是通过怎样的方式将模型提供的能力暴露出去,是我们与这个软件交互的入口。
- 实现,就是软件提供的模型和接口在内部是如何实现的,是软件能力得以发挥的根基。
了解设计的顺序应该是,**先模型,再接口,最后是实现**。了解设计,需要一层一层地展开,在每个层次都按照模型、接口和实现进行理解,在头脑中形成一棵设计树。
现在你已经有了一个了解设计的基本方法,接下来几讲,我会用几个开源项目带你再进一步,去看看如何去了解模型、接口和实现。
如果今天的内容你只能记住一件事,那请记住:**了解设计,先模型,再接口,最后是实现**。
<img src="https://static001.geekbang.org/resource/image/c3/37/c33374c66f20f52ce6119e64b53ae137.jpg" alt="">
## 思考题
现在的开源项目越来越多,每个开源项目都会提供一些不同的特点,请你找一些自己感兴趣的开源项目,看看它们分别提供了什么,是新的模型、是新的接口,还是新的实现?欢迎在留言区分享你的思考。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,217 @@
<audio id="audio" title="05 | Spring DI容器如何分析一个软件的模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5c/c7/5c330753ebb11f1414f523ab124c53c7.mp3"></audio>
你好!我是郑晔。
在上一讲中,我们讨论了如何了解一个软件的设计,主要是从三个部分入手:模型、接口和实现。那么,在接下来的三讲中,我将结合几个典型的开源项目,告诉你如何具体地理解一个软件的模型、接口和实现。
今天这一讲,我们就先来谈谈了解设计的第一步:模型。如果拿到一个项目,我们怎么去理解它的模型呢?
**我们肯定要先知道项目提供了哪些模型,模型又提供了怎样的能力。**这是所有人都知道的事情,我并不准备深入地去探讨。但如果只知道这些,你只是在了解别人设计的结果,这种程度并不足以支撑你后期对模型的维护。
在一个项目中,常常会出现新人随意向模型中添加内容,修改实现,让模型变得难以维护的情况。造成这一现象的原因就在于他们对于模型的理解不到位。
我们都知道,任何模型都是为了解决问题而生的,所以,理解一个模型,需要了解在没有这个模型之前,问题是如何被解决的,这样,你才能知道新的模型究竟提供了怎样的提升。也就是说,**理解一个模型的关键在于,要了解这个模型设计的来龙去脉,知道它是如何解决相应的问题。**
今天我们以Spring的DI容器为例来看看怎样理解软件的模型。
## 耦合的依赖
Spring在Java世界里绝对是大名鼎鼎如果你今天在做Java开发而不用Spring那么你大概率会被认为是个另类。
今天很多程序员都把Spring当成一个成熟的框架很少去仔细分析Spring的设计。但作为一个从0.8版本就开始接触Spring的程序员我刚好有幸经历了Spring从渺小到壮大的过程得以体会到Spring给行业带来的巨大思维转变。
如果说Spring这棵参天大树有一个稳健的根基那其根基就应该是 Spring的DI容器。DI是Dependency Injection的缩写也就是“依赖注入”。Spring的各个项目都是这个根基上长出的枝芽。
那么DI容器要解决的问题是什么呢它解决的是**组件创建和组装**的问题,但是为什么这是一个需要解决的问题呢?这就需要我们了解一下组件的创建和组装。
在前面的课程中,我讲过,软件设计需要有一个分解的过程,所以,它必然还要面对一个组装的过程,也就是把分解出来的各个组件组装到一起,完成所需要的功能。
为了叙述方便我采用Java语言来进行后续的描述。
我们从程序员最熟悉的一个查询场景开始。假设我们有一个文章服务ArticleService提供根据标题查询文章的功能。当然数据是需要持久化的所以这里还有一个ArticleRepository用来与持久化数据打交道。
熟悉DDD的同学可能发现了这个仓库Repository的概念来自于DDD。如果你不熟悉也没关系它就是与持久化数据打交道的一层和一些人习惯的Mapper或者DAOData Access Object类似你可以简单地把它理解成访问数据库的代码。
```
class ArticleService {
//提供根据标题查询文章的服务
Article findByTitle(final String title) {
...
}
}
interface ArticleRepository {
//在持久化存储中,根据标题查询文章
Article findByTitle(final String title)
}
```
在ArticleService处理业务的过程中需要用到ArticleRepository辅助它完成功能也就是说ArticleService要依赖于ArticleRepository。这时你该怎么做呢一个直接的做法就是在 ArticleService中增加一个字段表示ArticleRepository。
```
class ArticleService {
private ArticleRepository repository;
public Article findByTitle(final String title) {
// 做参数校验
return this.repository.findByTitle(title);
}
}
```
目前看起来一切都还好但是接下来问题就来了这个字段怎么初始化呢程序员一般最直接的反应就是直接创建这个对象。这里选用了一个数据库版本的实现DBArticleRepository
```
class ArticleService {
private ArticleRepository repository = new DBArticleRepository();
public Article findByTitle(final String title) {
// 做参数校验
return this.repository.findByTitle(title);
}
}
```
看上去很好但实际上DBArticleRepository并不能这样初始化。正如这个实现类的名字所表示的那样我们这里要用到数据库。但在真实的项目中由于资源所限我们一般不会在应用中任意打开数据库连接而是会选择共享数据库连接。所以DBArticleRepository需要一个数据库连接Connection的参数。在这里你决定在构造函数里把这个参数传进来。
```
class ArticlService {
private ArticleRepository repository;
public ArticlService(final Connection connection) {
this.repository = new DBArticleRepository(connection);
}
public Article findByTitle(final String title) {
// 做参数校验
return this.repository.findByTitle(title);
}
}
```
好,代码写完了,它看上去一切正常。如果你的开发习惯仅仅到此为止,可能你会觉得这还不错。但我们并不打算做一个只写代码的程序员,所以,我们要进入下一个阶段:测试。
一旦开始准备测试你就会发现要让ArticleService跑起来那就得让ArticleRepository也跑起来要让ArticleRepository跑起来那就得准备数据库连接。
是不是觉得太麻烦,想放弃测试。但有职业素养的你,决定坚持一下,去准备数据库连接信息。
然后,真正开始写测试时,你才发现,要测试,你还要在数据库里准备各种数据。比如,要测查询,你就得插入一些数据,看查出来的结果和插入的数据是否一致;要测更新,你就得先插入数据,测试跑完,再看数据更新是否正确。
不过,你还是没有放弃,咬着牙准备了一堆数据之后,你突然困惑了:我在干什么?我不是要测试服务吗?做数据准备不是测试仓库的时候应该做的事吗?
那么,问题出在哪儿呢?其实就在你创建对象的那一刻,问题就出现了。
## 分离的依赖
为什么说从创建对象开始就出问题了呢?
因为当我们创建一个对象时,就必须要有一个具体的实现类,对应到我们这里,就是那个 DBArticleRepository。虽然我们的ArticleService写得很干净其他部分根本不依赖于 DBArticleRepository只在构造函数里依赖了但依赖就是依赖。
与此同时由于要构造DBArticleRepository的缘故我们这里还引入了Connection这个类这个类只与DBArticleRepository的构造有关系与我们这个ArticleService的业务逻辑一点关系都没有。
所以,你看到了,只是因为引入了一个具体的实现,我们就需要把它周边配套的东西全部引入进来,而这一切与这个类本身的业务逻辑没有任何关系。
这就好像,你原本打算买一套家具,现在却让你必须了解树是怎么种的、怎么伐的、怎么加工的,以及家具是怎么设计、怎么组装的,而你想要的只是一套能够使用的家具而已。
这还只是最简单的场景,在真实的项目中,构建一个对象可能还会牵扯到更多的内容:
- 根据不同的参数,创建不同的实现类对象,你可能需要用到工厂模式。
- 为了了解方法的执行时间,需要给被依赖的对象加上监控。
- 依赖的对象来自于某个框架,你自己都不知道具体的实现类是什么。
- ……
所以,即便是最简单的对象创建和组装,也不像看起来那么简单。
既然直接构造存在这么多的问题,那么最简单的办法就是把创建的过程拿出去,只留下与字段关联的过程:
```
class ArticleService {
private ArticleRepository repository;
public ArticleService(final ArticleRepository repository) {
this.repository = repository;
}
public Article findByTitle(final String title) {
// 做参数校验
return this.repository.findByTitle(title);
}
}
```
这时候ArticleService就只依赖ArticleRepository。而测试ArticleService也很简单只要用一个对象将ArticleRepository的行为模拟出来就可以了。通常这种模拟对象行为的工作用一个现成的程序库就可以完成这就是那些Mock框架能够帮助你完成的工作。
或许你想问在之前的代码里如果我用Mock框架模拟Connection类是不是也可以呢理论上的确可以。但是想要让ArticleService的测试通过就必须打开DBArticleRepository的实现只有配合着其中的实现才可能让ArticleService跑起来。显然你跑远了。
现在,对象的创建已经分离了出去,但还是要要有一个地方完成这个工作,最简单的解决方案自然是,把所有的对象创建和组装在一个地方完成:
```
...
ArticleRepository repository = new DBArticleRepository(connection);
AriticleService service = new ArticleService(repository);
...
```
相比于业务逻辑,组装过程并没有什么复杂的部分。一般而言,纯粹是一个又一个对象的创建以及传参的过程,这部分的代码看上去会非常的无聊。
虽然很无聊但这一部分代码很重要最好的解决方案就是有一个框架把它解决掉。在Java世界里这种组装一堆对象的东西一般被称为“容器”我们也用这个名字。
```
Container container = new Container();
container.bind(Connection.class).to(connection);
container.bind(ArticleReposistory.class).to(DBArticleRepository.class);
container.bind(ArticleService.class).to(ArticleService.class)
ArticleService service = container.getInstance(ArticleService.class);
```
至此一个容器就此诞生。因为它解决的是依赖的问题把被依赖的对象像药水一样注入到了目标对象中所以它得名“依赖注入”Dependency Injection简称 DI。这个容器也就被称为DI容器了。
至此我简单地给你介绍了DI容器的来龙去脉。虽然上面这段和Spring DI容器长得并不一样但其原理是一致的只是接口的差异而已。
事实上这种创建和组装对象的方式在当年引发了很大的讨论直到最后Martin Fowler写了一篇《[反转控制容器和依赖注入模式](http://www.martinfowler.com/articles/injection.html)》的文章,才算把大家的讨论做了一个总结,行业里总算是有了一个共识。
那段时间DI容器也得到了蓬勃的发展很多开源项目都打造了自己的DI容器Spring是其中最有名的一个。只不过Spring并没有就此止步而是在这样一个小内核上面发展出了更多的东西这才有了我们今天看到的庞大的Spring王国。
讲到这里,你会想,那这和我们要讨论的“模型”有什么关系呢?
正如我前面所说,很多人习惯性把对象的创建和组装写到了一个类里面,这样造成的结果就是,代码出现了大量的耦合。时至今日,很多项目依然在犯同样的错误。很多项目测试难做,原因就在于此。这也从另外一个侧面佐证了可测试性的作用,我们曾在[第3讲](https://time.geekbang.org/column/article/241094)中说过:可测试性是衡量设计优劣的一个重要标准。
由此可见在没有DI容器之前那是怎样的一个蛮荒时代啊
有了DI容器之后呢你的代码就只剩下关联的代码对象的创建和组装都由DI容器完成了。甚至在不经意间你有了一个还算不错的设计至少你做到了面向接口编程它的实现是可以替换的它还是可测试的。与之前相比这是一种截然不同的思考方式而这恰恰就是DI容器这个模型带给我们的。
而且,一旦有了容器的概念,它还可以不断增强。比如,我们想给所有与数据库相关的代码加上时间监控,只要在容器构造对象时添加处理即可。你可能已经发现了,这就是 AOPAspect Oriented Programming面向切面编程的处理手法。而这些改动你的业务代码并无感知。
Spring的流行对于提升Java世界整体编程的质量是大有助益的。因为它引导的设计方向是一个好的方向一个普通的Java程序员写出来的程序只要符合Spring引导的方向那么它的基本质量就是有保障的远超那个随意写程序的年代。
不过如果你不能认识到DI容器引导的方向我们还是无法充分利用它的优势更糟糕的是我们也不能太低估一些程序员的破坏力。我还是见过很多程序员即便在用了Spring之后依然是自己构造对象静态方法满天飞把原本一个还可以的设计打得七零八落。
你看,通过上面的分析,我们知道了,只有理解了模型设计的来龙去脉,清楚认识到它在解决的问题,才能更好地运用这个模型去解决后面遇到的问题。如果你是这个项目的维护者,你才能更好地扩展这个模型,以便适应未来的需求。
## 总结时刻
今天,我们学习了如何了解设计的第一部分:看模型。**理解模型,要知道项目提供了哪些模型,这些模型都提供了怎样的能力**。但还有更重要的一步就是,**要了解模型设计的来龙去脉**。这样,一方面,可以增进了我们对它的了解,但另一方面,也会减少我们对模型的破坏或滥用。
我以Spring的DI容器为例给你讲解了如何理解模型。DI容器的引入有效地解决了对象的创建和组装的问题让程序员们拥有了一个新的编程模型。
按照这个编程模型去写代码整体的质量会得到大幅度的提升也会规避掉之前的许多问题。这也是一个好的模型对项目起到的促进作用。像DI这种设计得非常好的模型你甚至不觉得自己在用一个特定的模型在编程。
有了对模型的了解,我们已经迈出了理解设计的第一步,下一讲,我们来看看怎样理解接口。
如果今天的内容你只能记住一件事,那请记住:**理解模型,要了解模型设计的来龙去脉。**
<img src="https://static001.geekbang.org/resource/image/50/a2/50983d3d104c811f33f02db1783d4da2.jpg" alt="">
## 思考题
最后我想请你思考一个问题DI容器看上去如此地合情合理为什么在其他编程语言的开发中它并没有流行起来呢欢迎在留言区写下你的思考。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,256 @@
<audio id="audio" title="06 | Ruby on Rails如何分析一个软件的接口" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/85/db/85e4c18ba7a8b59c5c8a9d500c0142db.mp3"></audio>
你好!我是郑晔。
在上一讲中我以Spring的DI容器为例给你讲解了如何理解一个项目的模型。在模型之后下一步就该是接口了。
在任何一个项目中,接口的数量都不是一个小数目。仅仅一个普通的程序库,里面的接口少则几十个,多则成百上千。难道我们理解接口,就是要一个一个地读这些接口吗?
显然你不太可能把所有的接口细节都记住。我写Java程序差不多 20 年了但很多JDK里的类我都不了解。甚至有时候还没有等我去了解这个类它就过时了。
那么,如何才能从纷繁复杂的接口中,披荆斩棘而出呢?我给你个方法:**找主线,看风格**。
**找主线**的意思是,你需要找到一条功能主线,建立起对这个项目结构性的认知,而不是一上来就把精力放在每一个接口的细节上。你对细节部分的了解会随着你对项目的深入而逐渐增加。而有了主线后,你就有了着力点,就可以不断深入了。
但是,我们要学习的不只是这些接口的用法,要想从项目的接口设计上学到更多,这就需要你关注它所引导的**风格**,换句话说,就是它希望你怎样使用它,或是怎样在上面继续开发。
从一个项目的接口风格中,我们不难看出设计者的品位。我们常把编程视为一种艺术,而在接口的设计上就能窥见一二。这些内容是我们在学习软件设计时,应该慢慢品味的。
为什么要看风格,还有一个很重要的原因,就是你要维护项目的一致性,必须有一个统一的风格。有不少项目,里面会共存多种不同风格的接口,就是每个人都在设计自己习惯的接口,那势必会造成混乱。
这一讲我们就来一起来学习怎样看接口我选择的项目是Ruby on Rails因为它的接口设计风格是带给我最多震撼的无论是编程接口的优雅还是开发过程接口的顺畅。不过正如我在[第4讲](https://time.geekbang.org/column/article/241114)所说看设计要先看模型。所以我们还是先快速地了解一下Ruby on Rails的模型。
## Ruby on Rails模型
如果你是一个比较年轻的程序员Ruby on Rails这个名字可能会让你有一些陌生。但是在十多年前它初出茅庐之际可是给行业带来了极大的冲击。只是后来时运不济编程模型发生了大的改变使它失去了行业领导者的地位。这个故事还是要从你最熟悉的Web开发说起。
自从互联网兴起人们对于Web开发的探索就从未停止过。
最早期的Web开发只是静态页面的开发那时候你只要熟悉HTML就可以说自己懂Web开发了。后来人们不再满足于静态页面开始尝试编写有动态效果的页面。
一方面浏览器开始支持JavaScript让页面本身有了动态效果另一方面有人开始制作后台服务在页面之间切换的时候也可以有动态的效果。那个时候出现了像CGICommon Gateway Interface这样的编程规范。
当Java世界里出现了Servlet、JSP这些规范Web开发也逐渐由小打小闹变成了企业开发的主力越来越多的公司开始正视Web开发。因为这些规范很沉重一些号称要简化Web开发的框架开始出现比如Struts、Webwork以及Spring MVC等等。
这些框架的出现让Web开发摆脱了Servlet的初级阶段使MVC模式成为了Web开发的主流。但即便如此那个时候的Java Web开发依然是沉重的比如写一个Web应用光是配置文件就足以把人逼疯。
Ruby on Rails正是在这样的背景下横空出世的。为了叙述方便后面我就把Ruby on Rails简称Rails了。
从模型上讲Rails是标准的**基于MVC模型进行开发的Web框架**。在这一点上,它没有什么特殊的,它给行业带来巨大冲击的是它的接口设计。
Rails一个重要的设计理念就是**约定优于配置**无需配置按照缺省的风格就可以完成基本的功能这样的理念贯穿在Rails各个接口的设计中。
接下来我们就来看Rails的接口。
前面我提到过理解接口应该先找主线,**找到项目主线的一个方法就是从起步走文档开始,因为它会把项目最基本的用法展现给你,你可以轻松地找到主线**。
Rails的起步走文档做得就非常好主线可以说是一目了然。它用了一个Web项目帮你介绍了Rails开发的基本过程通过这个过程你就对Rails有了初步的印象。
有了主线之后我们就要开始从中了解接口的风格。Rails给我们提供的三种接口分别是
- Web应用对外暴露的接口REST API
- 程序员写程序时用到的接口API
- 程序员在开发过程中用到的接口:命令行。
接下来,我们就一个个地深入其中,了解它们的风格,以及它们给行业带来的不同思考。
## REST 接口
先说应用对外暴露的接口REST API。REST如今已经成为很多人耳熟能详的名词它把Web 的各种信息当作资源。既然是资源它就可以对这些Web信息做各种操作这些操作对应着HTTP的各种动词GET、POST、PUT、DELETE等
REST当年的问世是Roy Fielding博士为了纠正大家对HTTP的误用。 REST刚出来的时候开发者普遍觉得这是一个好的想法但怎么落地呢没有几个人想得清楚。
Rails恰逢其时地出现了。Rails对REST的使用方式做了一个约定。只要你遵循Rails的惯用写法写出来的结果基本上就是符合REST结构的也就是说Rails把REST这个模型用一种更实用的方式落地了。
```
Rails.application.routes.draw do
...
resources :articles
...
end
```
在用Rails写程序的时候你只要添加一个resource进去它就会替你规划好这个资源应该如何去写、怎么设计URL、用哪些HTTP动词以及它们对应到哪些方法。
```
$ bin/rails routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
root GET / welcome#index
```
看了Rails给你的这个映射关系后你就知道自己该怎么写代码了。这就是一种约定不需要你费心思考因为这是人家总结出来的行业中的最佳实践。只要按照这个规范写你写的就是一个符合REST规范的代码这就是Rails引导的外部接口风格。
## API 接口
我们再来看API接口。当年我接触Rails时最让我感到震惊的是它的数据库查询方式与传统开发的风格截然不同就这么简单的一句
```
Article.find_by_title(&quot;foo&quot;)
```
要知道那个时候用Java写程序即便是想做一个最简单的查询写的代码也是相当多的。我们不仅要创建一个对象还要写对应的SQL语句还要把查询出来的结果按照一定的规则组装起来。
而 Rails用一句轻描淡写find_by就解决了所有的问题而且这个find_by_title方法还不是我实现的Rails会替你自动实现。当我们需要有更多的查询条件时只要一个一个附加上去就可以了。
```
Article.find_by_title_and_author(&quot;foo&quot;, &quot;bar&quot;)
```
同样的事如果放到Java里去做还需要把前面说的事再做一遍差别只是查询语句不一样。
虽然我说的是当年的场景但时至今日在这些简单问题上很多使用Java的团队所付出的工作量并不比当年少。
从功能的角度说这样的查询在功能上是完全一样的但显然Rails程序员和Java程序员的工作量是天差地别的。这其中的差异就是不同的编程接口所造成的。
所以你看,一个好的接口设计会节省很多工作量,会减少犯错的几率。因为它会在背后帮你实现那些细节。
而设计不好的接口,则会把其中的细节暴露出来,让使用者参与其中。写程序库和写应用虽然都是写代码,但二者的要求确实相差极大。把细节暴露给所有人,显然是一个增加犯错几率的事情。
Rails的API接口给行业带来的另一个影响是它让人们开始关注API的表达性。比如每篇文章可以有多个评论用Rails的方式写出来是这样的
```
class Article &lt; ApplicationRecord
has_many :comments
...
end
```
而如果用传统Java风格你写出来的代码可能是这个样子的
```
class Article {
private List&lt;Comment&gt; comments;
...
}
```
很明显“有多个”这种表示关系的语义用has_many表示更为直白如果用List ,你是无法辨别它是一个属性,还是一个关系的。
Rails里面类似的代码有很多包括我们前面提到的find_by。所以如果你去读Rails写成的应用会觉得代码的可读性要好得多。
由于Rails的蓬勃发展人们也开始注意到好接口的重要性。Java后期的一些开源项目也开始向Rails学习。比如使用Spring Data JPA的项目后我们也可以写出类似Rails的代码。声明一对多的关系可以这样写
```
class Article {
@OneToMany
private List&lt;Comment&gt; comments;
...
}
```
而查询要定义一个接口,代码可以这样写:
```
interface ArticleRepository extends JpaRepository&lt;Article, Long&gt; {
Article findByTitle(String title);
Article findByTitleAndAuthor(String title, String author);
}
```
当你需要使用的时候,只要在服务里调用对应的接口即可。
```
class ArticleService {
private ArticleRepository repository;
...
public Article findByTitle(final String title) {
return repository.findByTitile(title);
}
}
```
显然Java无法像Rails那样不声明方法就去调用因为这是由Ruby的动态语言特性支持的而Java这种编译型语言是做不到的。不过比起从前自己写SQL、做对象映射已经减少了很多的工作量。
顺便说一下Spring Data JPA之所以能够只声明接口一个重要的原因就是它利用了Spring提供的基础设施也就是上一讲提到的依赖注入。它帮你动态生成了一个类不用你自己手工编写。
简单表达性好这就是Rails API的风格。
## 命令行接口
作为程序员我们都知道自动化的重要性但Rails 在“把命令行的接口和整个工程配套得浑然一体”这个方面做到了极致。Rails的自动化不仅会帮你做一些事情更重要的是它还把当前软件工程方面的最佳实践融合进去这就是Rails的命令行风格。
如果要创建一个新项目你会怎么做呢使用Rails这就是一个命令
```
$ rails new article-app
```
这个命令执行的结果生成的不仅仅是源码,还有一些鼓励你去做的最佳实践,比如:
- 它选择了Rake作为自动化管理的工具生成了对应的Rakefile
- 它选择了RubyGem作为包管理的工具生成了对应的Gemfile
- 为防止在不同的人在机器上执行命令的时间不同导致对应的软件包有变动生成了对应的Gemfile.lock锁定了软件包的版本
- 把对数据库的改动变成了代码;
- ……
而这仅仅是一个刚刚生成的工程,我们一行代码都没有写,它却已经可以运行了。
```
$ bin/rails server
```
这就启动了一个服务器,访问 [http://localhost:3000/](http://localhost:3000/) 这个 URL你就可以访问到一个页面。
如果你打算开始编写代码你也可以让它帮你生成代码骨架。执行下面的命令它会帮你生成一个controller类生成对应的页面甚至包括了对应的测试这同样是一个鼓励测试的最佳实践。
```
$ bin/rails generate controller Welcome index
```
在Rails蓬勃发展的那个时代人们努力探索着Web开发中各种优秀的做法而在这个方面走在最前沿的就是Rails。所以那个时候我们经常会关注Rails的版本更新看看又有哪些好的做法被融入其中。
Rails中那些优秀的实践逐步地被各种语言的框架学习着。语言编写者们在设计各种语言框架时也都逐步借鉴了Rails中的那些优秀实践。比如今天做Java开发我们也会用到数据库迁移的工具比如Flyway。
当然另一个方面即便到了今天大部分项目的自动化整合程度也远远达不到Rails的高度可能各方面的工具都有但是如此浑然一体的开发体验依然是Rails做得最好。
最后你可能会问Rails这么优秀为什么今天却落伍了呢
在Web开发领域Rails可谓成也MVC败也MVC。MVC是那个时代Web开发的主流页面主要在服务端进行渲染。然而后来风云突变拜JavaScript虚拟机V8所赐JavaScript能力越来越强Node.js兴起人们重新认识了JavaScirpt。它从边缘站到了舞台中心各种组件层出不穷前端页面的表现力大幅度增强。
Web开发的模式由原来的MVC逐渐变成了前端提供页面后端提供接口的方式。Java的一些框架和服务也逐步发展了起来Spring系列也越来越强大重新夺回了Web后端开发的关注。
**总结时刻**
今天,我们学习如何了解设计的第二部分:看接口。看接口的一个方法是**找主线,看风格**。先找到一条功能主线,对项目建立起结构性的了解。有了主线之后,再沿着主线把相关接口梳理出来。
查看接口,关键要看接口的风格,也就是项目作者引导人们怎样使用接口。在一个项目里,统一接口风格也是很重要的一个方面,所以,熟悉现有的接口风格,保持统一也是非常重要的。
我还介绍了一个曾经火爆的Web开发框架Ruby on Rails。借着它的起步走文档我给你介绍了它的一些接口包括
- Web应用对外暴露的接口REST API
- 程序员写程序时用到的接口API
- 程序员在开发过程中用到的接口:命令行。
从Rails的接口设计中我们可以看到一个好的接口设计无论是最佳实践的引入抑或是API设计风格的引导都可以帮助我们建立起良好的开发习惯。
当我们理解了模型和接口,接下来就该看实现了,这就是我们下一讲要讲的内容。
如果今天的内容你只能记住一件事,那请记住:**理解一个项目的接口,先找主线,再看风格。**
<img src="https://static001.geekbang.org/resource/image/11/a6/11a50512d7ff450654c9eb5977de33a6.jpg" alt="">
## 思考题
最后,我想请你来分享一下,你在哪个项目的设计中学到了一些好的开发习惯呢?欢迎在留言区分享你的经历。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,141 @@
<audio id="audio" title="07 | Kafka如何分析一个软件的实现" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/42/3f/42b364b6837233a14b8987d18ac91c3f.mp3"></audio>
你好!我是郑晔。
上一讲,我们学习了如何看接口,今天我们进入第三个部分——看实现。在一个系统中,模型和接口是相对稳定的部分。但是,同样的模型和接口,如果采用不同的实现,稳定性、可扩展性和性能等诸多方面相差极大。而且,只有了解实现,你才有改动代码的基础。
但是,不得不说,“看实现”是一个很大的挑战,因为有无数的细节在那里等着你。所以,在很多团队里,一个新人甚至会用长达几个月的时间去熟悉代码中的这些细节。
面对这种情况,我们该怎么办呢?
首先,你要记住一件事,你不太可能记住真实项目的所有细节,甚至到你离开项目的那一天,你依然会有很多细节不知道,可这并不妨碍你的工作。但是,如果你心中没有一份关于项目实现的地图,你就一定会迷失。
像我前面所说的新人,他们用几个月的时间熟悉代码,就是在通过代码一点点展开地图,但是,这不仅极其浪费时间,也很难形成一个整体认知。所以我建议,你应该直接把地图展开。怎么展开呢?**你需要找到两个关键点:软件的结构和关键的技术。**
可能你还不太理解我的意思下面我就以开源软件Kafka为例给你讲一下如何把地图展开去看一个软件的实现。按照我们之前讲过的思路了解一个软件设计的步骤是“先模型再接口最后看实现”。所以我们要先了解Kafka的模型和接口。
## 消息队列的模型与接口
Kafka是这么自我介绍的Kafka是一个分布式流平台。这是它现在的发展方向但在更多人的心目中Kafka的角色是一个消息队列。可以说消息队列是Kafka这个软件的核心模型而流平台显然是这个核心模型存在之后的扩展。所以我们要先把焦点放在Kafka的核心模型——消息队列上。
简单地说, 消息队列Messaging Queue是一种进程间通信的方式发消息的一方也就是生产者将消息发给消息队列收消息的一方也就是消费者将队列中的消息取出并进行处理。
站在看模型的角度上消息队列是很简单的无非是生产者发消息消费者消费消息。而且消息队列通常还会有一个topic的概念用以区分发给不同目标的消息。
消息队列的基本接口也很简单。以Kafka为例生产者是这样发消息的
```
producer.send(new KafkaRecord&lt;&gt;(&quot;topic&quot;, new Message()));
```
而消费者收消息是这样的:
```
ConsumerRecords&lt;String, Message&gt; records = consumer.poll(1000);
```
有了对模型和接口的基本了解,我们会发现,消息队列本身并不难。
但我们都知道消息队列的实现有很多Kafka只是其中一种还有诸如ActiveMQ、RabbitMQ等的实现。为什么会有这么多不同的消息队列实现呢因为每个消息队列的实现都会有所侧重不同的消息队列有其适用的场景。
消息队列还有一个最常见的特性是,它会提供一定的消息存储能力。这样的话,当生产者发消息的速度快于消费者处理消息的速度时,消息队列可以起到一定的缓冲作用。所以,有一些系统会利用消息队列的这个特性做“削峰填谷”,也就是在消息量特别大时,先把消息收下来,慢慢处理,以减小系统的压力。
Kafka之所以能从一众消息队列实现中脱颖而出一个重要的原因就是它针对消息写入做了优化它的生产者写入速度特别快。从整体的表现上看就是吞吐能力特别强。
我们已经对Kafka的能力有了一个初步的认识。显然介绍接口和模型不足以将它与其他消息队列实现区分开来。所以我们必须拉开大幕开始去了解它的实现。
## 软件的结构
前面我提到,**当我们想去看一个软件的实现时,有两件事特别重要:软件的结构和关键的技术**。
我们先来看软件的结构。软件的结构其实也是软件的模型,只不过,它不是整体上的模型,而是展开实现细节之后的模型。我在[第1讲](https://time.geekbang.org/column/article/240177)也说过,模型是分层的。
对于每个软件来说,当你从整体的角度去了解它的时候,它是完整的一块。但当你打开它的时候,它就变成了多个模块的组合,这也是所谓“分层”的意义所在。而上一层只要使用下一层提供给它的接口就好。
所以,当我们打开了一个层次,了解它的实现时,也要先从大处着手。最好的办法就是我们能够**找到一张结构图**,准确地了解它的结构。
如果你能够找到这样一张图,你还是很幸运的。因为在真实的项目中,你可能会碰到各种可能性:
- 结构图混乱:你找到一张图,上面包含了各种内容。比如,有的是模块设计,有的是具体实现,更有甚者,还包括了一些流程;
- 结构图复杂:一个比较成熟的项目,图上画了太多的内容。确实,随着项目的发展,软件解决的问题越来越多,它必然包含了更多的模块。但对于初次接触这个项目的我们而言,它就过于复杂了;
- 无结构图:这是最糟糕的情况,你最好先想办法画出一张图来。
无论遇到上述的哪种情况,你了解项目都不会很顺利。所以,你还是要先了解模型和接口,因为它们永远是你的主线,可以帮你从混乱的局面中走出来。
那么,假设现在你有了一张结构图,在我们继续前进之前,我想先问一个问题:现在你有了一张结构图,你打算做什么?你可能会问,难道不是了解它的结构吗?是,但不够。我们不仅要知道一个设计的结果,最好还要推断出**设计的动因**。
所以一种更好的做法是带着问题上路。我们不妨假设自己就是这个软件的设计者问问自己要怎么做。然后再去对比别人的设计你就会发现自己的想法和别人想法的相同或不同之处。对于理解Kafka而言第一个问题就是**如果你来设计一个消息队列,你会怎么做呢?**
如果在网上搜索Kafka的架构图你会搜到各种各样的图上面包含了不同的信息。有的告诉你分区Partition的概念有的告诉你Zookeeper。根据前面对模型的介绍我特意挑了一张看上去最简单的架构图因为它最贴近消息队列的基础模型
<img src="https://static001.geekbang.org/resource/image/ee/79/ee05f6c6446600da97d824591e5a4d79.jpg" alt="">
那么从这个图中你能看到什么呢你能看到Kafka的生产者一端将消息发送给Kafka集群然后消费者一端将消息取出来进行处理。这样的结构和你想的是不是一样的呢如果让你负责进一步设计你会怎么做呢
- 生产者端封装出一个 SDK负责消息的发送
- 消费者端封装出一个 SDK负责消息的接收
- 设计一个集群系统,作为生产者和消费者之间的连接。
然后,你就可以问自己更多的问题:
- 生产端如果出现网络抖动,消息没有成功发送,它要怎么重试呢?
- 消费端处理完的消息,怎样才能保证集群不会重复发送呢?
- 为什么要设计一个集群呢?要防止出现单点的故障,而一旦有了集群,就会牵扯到下一个问题,集群内的节点如何保证消息的同步呢?
- 消息在集群里是怎么存储的?
- 生产端也好,消费端也罢,如果一个节点彻底掉线,集群该怎么处理呢?
- ……
你有了更多的问题之后,你就会在代码里进行更深入地探索。你可以根据需要,打开对应模块,进一步了解里面的实现。比如,消息重发的问题,你就可以看看生产端是怎么解决这些问题的。当问题细化到具体实现时,我们就可以打开对应的源码,去里面寻找答案。
从结构上来说Kafka不是一个特别复杂的系统。所以如果你的项目更复杂层次更多我建议你把各个层次逐一展开先把整体结构放在心中再去做细节的探索。
## 关键的技术
我们再来看看理解实现的另一个重要方面:关键技术。
什么算是关键技术呢?就是能够让这个软件的“实现”与众不同的地方。了解关键技术可以保证一点,就是我们对代码的调整不会使项目出现明显的劣化。幸运的是,大多数项目都会愿意把自己的关键技术讲出来,所以,找到这些信息并不难。
以Kafka为例前面说过它针对写入做了优化使得它的整体吞吐能力特别强。那它是怎么做到的呢
消息队列实现消息存储的方式通常是把它写入到磁盘中而Kafka的不同之处在于它利用了**磁盘顺序读写**的特性。对于普通的机械硬盘而言,如果是随机写,需要按照机械硬盘的方式去寻址,然后磁头做机械运动,写入速度就会慢得多。但顺序写的话,会大幅度减少磁头的运动,效率自然就得到了大幅度的提高。
之所以可以这样实现,是充分利用了消息队列本身的特性:有序。它是技术实现与需求完美结合的产物。有了这个基础,就可以有进一步的优化。比如,利用内存映射文件减少用户空间到内核空间复制的开销。
如果站在了解实现的角度,你会觉得非常地自然。但要想从设计的角度学到更多,我们还是应该带着问题上路,多问自己一个问题,为什么其他的消息队列之前不这么做呢?这是一个值得深思的问题。**Kafka这个实现到底是哪里不容易想到呢答案是软硬结合。**
之前的消息队列实现也会把消息写入到文件里但文件对它们来说只是一个通用的接口。开发者并没有想过利用硬件的特性做开发。而Kafka的开发者突破了这个限制把硬件特性利用了起来从而取得了更好的结果。
一旦理解了这一点,我们再来看其他的一些设计,就能学到更多的东西。比如,有一个著名的开源项目[LMAX Disruptor](http://lmax-exchange.github.io/disruptor/),它号称是最强劲的线程通信库。它有一段非常奇怪的代码,类似这样:
```
protected long p1, p2, p3, p4, p5, p6, p7;
```
以正常程序员的标准这简直是无厘头的低劣代码。而想要理解这段代码你必须理解CPU缓存行的机制这也是一种软硬结合的思路。
对于习惯写“软”件的程序员而言,在软件上投入的努力到达极限时,软硬结合是一种思路上的突破。当然,这种突破的前提是要对硬件的机制有所了解,这往往是很多程序员在基本功上欠缺的,可以学习一下计算机组成原理之类的课程。如果你有时间去学习,《[深入理解计算机系统](http://book.douban.com/subject/26912767/)》一书值得一读。
## 总结时刻
今天是了解设计的第三部分:看实现。理解一个实现,是以对模型和接口的理解为前提的。
每个系统的实现都有非常多的细节,我们不可能一上来就把所有的细节吃透。如果想了解一个系统的实现,应该从**软件结构**和**关键技术**两个方面着手。无论是软件结构,还是关键技术,我们都需要带着自己的问题入手,而问题的出发点就是我们对模型和接口的理解。
了解软件的结构,其实,就是把分层的模型展开,看下一层的模型。一方面,你要知道这个层次给你提供了怎样的模型,另一方面,你要带着自己的问题去了解这些模型为什么要这么设计。
最后我借着Kafka的关键技术还给你讲了**软硬结合**的思路在系统优化之路上寻求突破时可以增加你选择的道路。不过实现都是有约束的比如Kafka的实现主要是针对机械硬盘做的优化现在的SSD硬盘越来越多成本越来越低这个立意的出发点已经不像以前那样稳固了。
至此,了解设计的三步我们已经全部走完了。接下来,我们就要开始自己的设计历程了。首先,我们需要掌握一些关于设计的基础知识。下一讲,我们就从最基础的部分入手,我们来谈谈程序设计语言。
如果今天的内容你只能记住一件事,那请记住:**理解实现,带着自己的问题,了解软件的结构和关键的技术。**
<img src="https://static001.geekbang.org/resource/image/29/ef/29c6a18e1e1313ff0e6c7aad3642f3ef.jpg" alt="">
## 思考题
最后,我想请你来思考一下,在项目上学习的哪些东西对你个人在实现思路上有了一个极大的突破。欢迎在留言区分享你的经历。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。