Skip to content

《架构整洁之道》

  • 介绍:《架构整洁之道》读书笔记。

  • 评价 : 很不错的一本书!认真读完之后,我保证你对编程本质、编程语言的本质、软件设计、架构设计可以有进一步的认识。国内的很多书籍和专栏都借鉴了《架构整洁之道》 这本书。毫不夸张地说,《架构整洁之道》就是架构领域最经典的书籍之一。正如作者说的那样:“如果深入研究计算机编程的本质,我们就会发现这 50 年来,计算机编程基本没有什么大的变化。编程语言稍微进步了一点,工具的质量大大提升了,但是计算机程序的基本构造没有什么变化。虽然我们有了新的编程语言、新的编程框架、新的编程范式,但是软件架构的规则仍然和 1946 年阿兰·图灵写下第一行机器代码的时候一样”。这本书就是为了把这些永恒不变的软件架构规则展现出来。不过,缺点还是有的,内容过于理论话,后半部分的内容稍微有点枯燥。而且,有些地方讲解的不是很清楚(也可能是翻译的问题)。

  • 作者:Guide哥

image-20220218130920537.png

1.概述

如果深入研究计算机编程的本质,我们就会发现这 50 年来,计算机编程基本没有什么大的变化。编程语言稍微进步了一点,工具的质量大大提升了,但是计算机程序的基本构造没有什么变化。

虽然我们有了新的编程语言、新的编程框架、新的编程范式,但是软件架构的规则仍然和 1946 年阿兰·图灵写下第一行机器代码的时候一样。

这本书就是为了把这些永恒不变的软件架构规则展现出来。

好的软件架构可以大大节省项目构建和维护的人力成本。让每次变更都短小简单,易于实施。

设计与架构究竟是什么

软件架构的终极目标

软件架构的终极目标是用最少的人力成本即可满足构建和维护该系统的需求。

有没有什么办法判断软件架构设计的好不好么?

如果一个系统每次发布新版本都会提升下一次变更的成本,这个系统的架构设计就是不好的!比如说随着产品的发布,产品的工程师人数直线上涨,但是系统代码生产效率却没有得到相应的提高。

大部分工程师都会低估好的系统设计、代码整洁的重要性。这些工程师普遍喜欢用:“我们可以未来重构代码,产品上线最重要”。然而,结果是产品上线之后重构就没人提了。

所以,重构的时机永远不会再有了。工程师们忙于完成新功能,新功能做不完,哪有时间重构老的代码?循环往复,系统成了一团乱麻,生产效率持续直线下降,直至为零。


个人思考

现实世界中有一些人你真的叫不醒,他们对于自己的做的东西盲目自信,一碰到自己看不懂的代码就说别人写的花里胡哨。每当你说要注意代码质量,系统架构设计,他就用各种理由搪塞你。最典型的就是拿项目上线比较紧张为理由。是的!我也知道产品上线之前的那种压力有多大。但是,我们要明确的一点是现在埋得坑未来要多花一倍甚至是几倍的时间来填。一个好的 Coder 先从不给别人挖坑做起!

然而,项目 Leader 不给我们时间去重构,他的眼里只有完成新功能并上线,毕竟系统架构/代码质量的提升并不会让不懂代码的人感觉到这个东西的意义有多大。有些人只关注外在,内在的东西再怎么烂,我都不管。

然而,很多时候我们加班都是因为系统没有设计好或者代码质量太糟糕。如此一来,便造成了恶性循环。大部分无辜的程序员同胞直呼:“ 我们只想安安静静地当个写需求的工具人啊!”。

客观地说,国内的各种系统、开源项目整体的代码质量和设计确实很难找到一个比较优秀的。单提这一点来说我们确实还要努力!认清自己的不足,才有希望缩小与别人的差距。不过,目前确实在慢慢进步中,国产程序员们加油!!!

两个价值的维度

对于每个软件系统,我们可以通过行为和架构两个维度来体现它的价值。

行为价值

软件系统的行为是其最直观的价值维度。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润。程序员们为了达到这个目的,往往需要帮助系统使用者编写一个对系统功能的定义,也就是需求文档。然后,程序员们再把需求文档转化为实际的代码。

架构价值

软件系统的第二个价值维度,就体现在软件这个英文单词上: software。“ware”的意思是“产品”,而“soft”的意思,不言而喻,是指软件的灵活性。

个人思考

非技术人员比如业务分析师、项目经历等角色通常认为系统能正确提供服务就行了,完全不关心系统的架构是否灵活。我们作为系统的开发人员通常也会跟随这些角色采取同样的态度。

《架构整洁之道》有一句话说的很好:

如果忽视软件架构的价值,系统将会变得越来越难以维护,终会有一天,系统将会变得再也无法修改。如果系统变成了这个样子,那么说明软件开发团队没有和需求方做足够的抗争,没有完成自己应尽的职责。

一言以蔽之,软件变得越来越难维护,我们作为开发人员一定要反思一下自己的问题。

2.编程范式

编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构。直到今天,我们也一共只有三个编程范式,而且未来几乎不可能再出现新的。并且,这三个编程范式都是在 1958 年到 1968 年这 10 年间被提出来的。

结构化编程

Dijkstra 在研究过程中发现了一个问题: goto 语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。

goto 语句的其他用法虽然不会导致这种问题,但是 Dijkstra 意识到它们的实际效果其实和更简单的分支结构 if-then-else 以及循环结构 do-while 是一致的。如果代码中只采用了这两类控制结构,则一-定可以将程序分解成更小的、可证明的单元。

结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。这就是为什么现代编程语言- -般不支持无限制的 goto 语句。更重要的是,这也是为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一

无论在哪一个层面上,从最小的函数到最大组件,软件开发的过程都和科学研究非常类似,它们都是由证伪驱动的。软件架构师需要定义可以方便地进行证伪(测试)的模块、组件以及服务。为了达到这个目的,他们需要将类似结构化编程的限制方法应用在更高的层面上。

面向对象编程

封装

把类中的私有属性和公共的函数绑定在一起,只能通过函数改变私有属性。

虽然 C 语言是非面向对象编程语言,但是,C 语言中的才是完美封装。

我们很难说封装是面向对象编程的必要条件,很多面向对象编程语言对封装性也并没有强制性的要求。

面向对象编程会尽量要求程序员尽量避免破话数据的封装性,但是,实际上,那些声称自己提供面向对象编程支持的编程语言,相对于 C 这种完美封装的语言而言,其封装性都被削弱了。

继承

面向对象编程语言并没有提供更好的封装性,那么在继承方面有如何呢?

其实也一般般。

面向对象编程语言在发明之前,继承特性就已经存在了。只不过面向对象编程语言让继承更加容易使用了!

多态

面向对象编程语言发明之前,我们所使用的一些编程语言比如 C 也是能够支持多态的。

归根结底,多态其实就是函数指针的一种应用。自从 20 世纪 40 年代末期冯诺依曼架构诞生那天起,程序员就一直在使用函数指针模拟多态了。

面向对象编程并没有在多态方面提出任何新概念,但是,他确实让多态变得更加安全、更加便于使用!

总结

面向对象编程到底是什么?业界在这个问题上存在着很多不同的说法和意见。然而对一一个软件架构师来说,其含义应该是非常明确的:面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。

函数式编程

为什么不可变性是软件架构设计需要考虑的重点呢?为什么软件架构师要操心变量的可变性呢?

答案显而易见:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。换句话说,一切并发应用遇到的问题,一切由于使用多线程、多处理器而引起
的问题,如果没有可变变量的话都不可能发生。

一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。

软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。

总结

下面我们来总结一下:

  • 结构化编程是对程序控制权的直接转移的限制。
  • 面向对象编程是对程序控制权的间接转移的限制。
  • 函数式编程是对程序中赋值操作的限制。

这三个编程范式都对程序员提出了新的限制。每个范式都约束了某种编写代码的方式,没有一个编程范式是在增加新能力。

也就是说,我们过去 50 年学到的东西主要是:什么不应该做。

我们必须面对这种不友好的现实:软件构建并不是一个迅速前进的技术。今天构建软件的规则和 1946 年阿兰·图灵写下电子计算机的第一行代码时是一样的。尽管工具变化了,硬件变化了,但是软件编程的核心没有变。

总而言之,软件,或者说计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加,也缺一不可。

3.设计原则

SOLID 原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。

请注意,这里虽然用到了“类”这个词,但是并不意味着我们将要讨论的这些设计原则仅仅适用于面向对象编程。这里的类仅仅代表了一种数据和函数的分组,每个软件系统都会有自己的分类系统,不管它们各自是不是将其称为“类”,事实上都是 SOLID 原则的适用领域。

SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:

  1. **单一职责原则(Single Responsibllity Principle - 即 SRP) ** : 该设计原则是基于康威定律(Conway's Law) '的一个推论—一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。
  2. 开闭原则(Open/Closed Principle - 即 OCP :该设计原则是由 Bertrand Meyer 在 20 世纪 80 年代大力推广的,其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
  3. 里式替换原则(Liskov Substitution Principle - 即 LSP :该设计原则是 Barbara Liskov 在 1988 年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
  4. 接口隔离原则(nterface Segregation Principle - 即 ISP :这项设计原则主要用来告诫软件设计师应该在设计张避免不必要的依赖。
  5. 依赖反转原则(Dependency Inversion Principle - DIP :该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。

单一职责原则(SRP)

SRP 的最终描述:任何一个模块都应该只对某一类行为负责。 (我觉得这种描述最好!最能体现 SRP 具体要表达的意思。)

大部分情况,“软件模块”都可以被简单的定义为一个源代码文件。但是,有些编程语言和编程环境不用源代码来存储程序,这些情况下,“软件模块”指的就是一组紧密相关的函数和数据结构。

个人思考

做好每一个组件,每个组件只专注某一类行为。比如说 MySQLRedisES 都有自己存储的 Repository。调用外部 RPC,RESTful API,都有自己的 client or facade or adpater(每个团队习惯不同)。调用 MQ,或者发布事件,都有对应的 producer 类。短信/邮件/文件服务,也有单独的 service。

开闭原则(OCP)

开闭原则(OCP)是 Bertrand Meyer 在 1988 年提出的,该设计原则认为: 设计良好的计算机软件应该易于扩展,同时抗拒修改。

换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。

其实,这也是我们研究软件架构的根本目的。如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的。尽管大部分软件设计师都已经认可了 OCP 是设计类与模块时的重要原则,但是在软件架构层面,这项原则的意义则更为重大。

个人思考

这个修改指的并不是我们不增加新的代码/组件,而是,我们可以在不修改或者只修改少量原有代码的情况下扩展程序的功能,在不修改原有软件架构的情况下,让软件适应新的需求,拥有新的功能。就比如现在经常会用到的基于插件的软件架构。

里式替换原则(LSP)

1988 年,Barbara Liskov 在描述如何定义子类型时写下了这样一段话:

这里需要的是一种可替换性:如果对于每个类型是 S 的对象 o1 都存在一个类型为 T 的对象 o2,能使操作 T 类型的程序 P 在用 o2 替换 ol 时行为保持不变,我们就可以将 S 称为 T 的子类型。

这句话所体现的设计理念就是里氏替换原则(LSP),不过不太好理解,我们可以来看几个例子。

继承的使用

1.假设我们有一个 License 类,其结构如下图所示。该类中有一个名为 calcFee() 的方法,该方法将由 Billing 应用程序来调用。而 License 类有两个“子类型”: PersonalLicenseBusinessLicense,这两个类会用不同的算法来计算授权费用。

20210427124531778.png

上述设计是符合 LSP 原则的,因为 Billing 应用程序的行为并不依赖于其使用的任何一个衍生类。也就是说,这两个衍生类对象都是可以用来替换 License 类对象的。

LSP 与软件架构

面向对象编程兴起的时候,大家只是普遍认为 LSP 只不过是指导我们如何使用继承关系的一种方法。然而,随着时间的推移, LSP 逐渐演变成了一种更广泛的、指导接口与其实现方式的设计原则。

这里提到的接口可以有多种形式一可 以是 Java 风格的接口,具有多个实现类;也可以像 Ruby 一样,几个类共用一样的方法签名,甚至可以是几个服务响应同一个 REST 接口。

LSP 适用于上述所有的应用场景,因为这些场景中的用户都依赖于一种接口,并且都期待实现该接口的类之间能具有可替换性。

LSP 可以且应该被应用于软件架构层面,因为一旦违背了可替换性,该系统架
构就不得不为此增添大量复杂的应对机制。

接口隔离原则(ISP)

任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

依赖反转原则(DIP)

DIP 主要告诉我们的是:如果要设计一个灵活的系统,在源代码层级的依赖关系中就应该多应用抽象类型,而非具体实现。

4.组件构建原则

组件

组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最.小实体。例如,对于 Java 来说,它的组件是 jar 文件。

我们可以将多个组件链接成一个独立可执行文件,也可以将它们汇总成类似.war 文件这样的部署单元,又或者,组件也可以被打包成.jar、.d11 或者.exe 文件,并以可动态加载的插件形式来独立部署。但无论采用哪种部署形式,设计良好的组件都应该永远保持可被独立部署的特性,这同时也意味着这些组件应该可以被单独开发。

组件聚合

究竟哪些类应该被组合成一个组件呢? 很多年来,我们都是临时拍脑门决定。

3 个与构建组件相关的基本原则:

  • REP :复用/发布等同原则
  • CCP :共用闭包原则
  • CRP : 共同复用原则

REP(复用/发布等同原则)

软件复用的最小粒度应等同于其发布的最小粒度。

从软件设计和架构设计的角度来说,REP 原则指的是组件中的类与模块必须是紧密相关的。也就是说,一个组件不能有一组毫无关联的类和模块组成,它们之间应该又一个共同的主题或者大方向。

REP 原则存在一些薄弱性,比如它没有清晰地定义出应该如何将类与模块组合成组件。

REP 原则的薄弱性由下面两个原则所补充。

CCP(共用闭包原则)

我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。 也就是说如果两个类紧密相关,不管是源代码层面还是抽象概念层面,永远都会一起呗修改,那么它们就应该归属于同一个组件。

SRP 原则中提到的“一个类不应该同时存在着多个变更原因”一样,CCP 原则也认为一个组件不应该同时存在着多个变更原因。

对大部分应用程序来说,可维护性的重要性要远远高于可复用性。

CRP (共同复用原则)

不要强迫一个组件依赖它不需要的东西。

组件聚合三大原则张力图

20210514163920236.png

优秀的软件架构师应该能够在上述三角张力区域中定位一个最适合目前研发团队状态的位置,同时会根据时间不断调整。

组件耦合

下面要讨论的 3 条原则主要关注的是组件之间的关系。

无依赖环原则

组件依赖关系中图中不应该出现环。

组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件

另外,随着应用程序的增长,创建可重用组件的需要也会逐渐重要起来。

如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。因此,组件依赖关系是必须要随着项目的逻辑设计一起扩张和演进的。

稳定依赖原则

依赖关系必须要指向更稳定的方向。

任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以修改。

稳定抽象原则

如果一个组件想要成为稳定组件,那么它就应该由接口和抽象类组成,以便将来做扩展。如此,这些既稳定又便于扩展的组件可以被组合成既灵活又不会受到过度限制的架构。

5.软件架构

什么是软件架构

架构师本身必须是一线程序员,并且,他们往往是团队中能力最强的程序员。架构师会逐渐引导团队向一个能够最大化生产力的系统设计方向前进。

软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。

开发

一个开发起来很困难的软件系统一般不太可能会有 一个长久、健康的生命周期,所以系统架构的作用就是要方便其开发团队对它的开发。

部署

为了让开发成为有效的工作,软件系统就必须是可部署的。在通常情况下,一个系统的部署成本越高,可用性就越低。因此,实现一键式的轻松部署应该是我们设计软件架构的一个目标。

运行

设计良好的系统架构应该可以让开发人员对系统的运行过程一目了然。架构应该起到揭示系统运行过程的作用。也就是说架构应该将系统中的用例、功能以及该系统的必备行为设置为对开发者可见的一级实体,简化他们对于系统的理解。这将为整个系统的开发和维护提供很大的帮助。

维护

在软件系统的所有方面中,维护所需的成本是最高的。满足永不停歇的新功能需求,以及修改层出不穷的系统缺陷这些工作将会占去绝大部分的人力资源。

独立性

按层解耦

从用例的角度来看,架构师的目标是让系统结构支持其所需要的所有用例。但是问题恰恰是我们无法预知全部的用例。好在架构师应该还是知道整个系统的基本设计意图的。也就是说,架构师应该知道自己要设计的是一个购物车系统,或是运输清单系统,还是订单处理系统。所以架构师可以通过采用单一职贵原则(SRP)和共同闭包原则(CCP),以及既定的系统设计意图来隔离那些变更原因不同的部分,集成变更原因相同的部分。

重复

不要转牛角尖—害怕重复。有些时候,重复也是必要的。比如当我们对系统进行水平分层时,也可能会发现某个数据库记录的结构和某个屏幕展示的数据接口非常相似。我们可能也会为了避免再创建一个看起来相同的视图模型并在两者之间复制元素,而选择直接将数据库记录传递给 UI 层(这是一种种表面性的重复。而且,另外创建一个视图模型并不会花费太多力气,这可以帮助我们保持系统水平分层之间的隔离)。

划分边界 👍

软件架构本身就是一门划分边界的艺术。边界的作用就是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。

软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、 可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,而这些其他组件要么是可以去掉的,要么是有多种实现的。

边界剖析

系统架构中最强的边界形式就是服务。一个服务就是一个进程,它们通常由命令行环境或其他等价的系统调用来产生。服务会始终假设它们之间的通信将全部通过网络进行。服务之间的跨边界通信相对于函数调用来说,速度是非常缓慢的,其往返时间.可以从几十毫秒到几秒不等。因此我们在划分架构边界时,一定要尽可能地控制通信次数。

除单体结构以外,大部分系统都会同时采用多种边界划分策略。一个按照服务层次划分边界的系统也可能会在某一部分采用本地进程的边界划分模式。事实上,服务经常不过就是一系列互相作用的本地进程的某种外在形式。无论是服务还是本地进程,它们几乎肯定都是由一个或多个源码组件组成的单体结构,或者一组动态链接的可部署组件。

这也意味着一个系统中通常会同时包含高通信量、低延迟的本地架构边界和低通信量、高延迟的服务边界。

awesome 的软件架构 👍

一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。这就好像一个住宅建筑设计的首要目标应该是满足住宅的使用需求,而不是确保一定 要用砖来构建这个房子。

架构师应该花费很多精力来确保该架构的设计在满足用例需要的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。而且,良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、Web 服务以及其他与环境相关的工具。框架应该是一个可选项,良好的架构设计应该允许用户在项目后期再决定是否采用 Rails、Spring、Hibernate. Tomecat、MySQL 这些工具。同时,良好的架构设计还应该让我们很容易改变这些决定。总之,良好的架构设计应该只关注用例,并能将它们与其他的周边因素隔离。

整洁架构 👍

过去几十年有很多系统架构的想法被提出,虽然这些架构在细节上各有不同,但总体来说都是非常相似的。它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。

按照这些架构设计出来的系统,通常都具有以下特点:

  • 独立于框架 : 这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。框架可以被当成工具来使用,但不需要让系统来适应框架。
  • 可被测试 : 这些系统的业务逻辑可以脱离 UI、数据库、Web 服务以及其他的外部元素来进行测试。
  • 独立于 UI : 这些系统的 UI 变更起来很容易,不需要修改其他的系统部分。例如,我们可以在不修改业务逻辑的前提下将一个系统的 UI 由 Web 界面替
    换成命令行界面。
  • 独立于数据库 : 我们可以轻易将这些系统使用的 Oracle、SQL Server 替换成 Mongo、BigTable、 CouchDB 之类的数据库。因为业务逻辑与数据库之间已经完成了解耦。
  • 独立于任何外部机构 : 这些系统的业务逻辑并不需要知道任何其他外部接口
    的存在。

下面我们要通过图 将上述所有架构的设计理念综合成为一个独立的理念。

20210520111652916.png

通常越靠近中心,其所在的软件层次就越高。

业务实体这一层中封装的是整个系统的关键业务逻辑,一个业务实体既可以是一个带有方法的对象,也可以是一组数据结构和函数的集合。无论如何,只要它能被系统中的其他不同应用复用就可以。

软件的用例层中通常包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例。这些用例引导了数据在业务实体之间的流入流出,并指挥着业务实体利用其中的关键业务逻辑来实现用例的设计目标。

软件的接口适配器层中通常是一组数据转换器,它们负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及 Web)最方便操的格式。

最外层的模型层一般是由工具、数据库、Web 框架等组成的。在这一层中,我们通常只需要编写一些与内层沟通的黏合性代码。框架与驱动程序层中包含了所有的实现细节。Web 是一个实现细节,数据库也是一个实现细节。我们将这些细节放在最外层,这样它们就很难影响到其他层了。

测试边界

测试并不是独立于整个系统之外的,恰恰相反,它们是系统的一个重要组成部.分。我们需要精心设计这些测试,才能让它们发挥验证系统稳定性和预防问题复发的作用。没有按系统组成部分来设计的测试代码,往往是非常脆弱且难以维护的。这种测试最后常常会被拋弃,因为它们终究会出问题。

软件构建过程的三个阶段 👍

Kent Beck 描述了软件构建过程中的三个阶段:

  1. “先让代码工作起来”一如果代码不能工作,就不能产生价值。
  2. “ 然后再试图将它变好”一通过对代码进行重构, 让我们自已和其他人更好地理解代码,并能按照需求不断地修改代码。
  3. “ 最后再试着让它运行得更快”一按照性 能提升的“需求”来重构代码。

6.实现细节

数据库只是实现细节

从系统架构的角度来看,数据库并不重要一它只是一个实现细节, 在系统架构中并不占据重要角色。如果就数据库与整个系统架构的关系打个比方,它们之间就好比是门把手和整个房屋架构的关系。

数据库终究只是在硬盘和内存之间相互传输数据的一种手段而已。

过去几十年内,业界逐渐发展出了两种截然不同的系统 :

  • 文件系统 :文件系统是基于文档格式的,它提供的是一种便于存 储整个文档的方式。当需按照名字存储数据和查找一系列文档时,文件系统很有用,但当我们需要检索文档内容时,它就没那么有用了。也就是说,我们在文件系统中查找一个名字为
  • 关系型数据库系统( RDBMS) : 数据库系统则主要关注的是内容,它提供的是一种便于进行内容检索的存储方式。其最擅长的是根据某些共同属性而检索一系列记录。

数据的组织结构,数据的模型,都是系统架构中的重要部分,但是从磁盘上存储/读取数据的机制和手段却没那么重要。关系型数据库强制我们将数据存储成表格并且以 SQL 访问,主要是为了后者。总而言之,数据本身很重要,但数据库系统仅仅是一个实现细节。

Web 是实现细节

软件是用 GUI 还是 命令行属于实现细节,Web 是 GUI 的一种,因此,也属于实现细节。作为一名软件架构师,我们需要将这类细节与核心业务逻辑隔离开来。

应用程序框架只是实现细节

应用程序框架现在非常流行,这在通常情况下是一件好事。许多框架都非常有效,非常有用,而且是免费的。但框架并不等同于系统架构一尽 管有些框架确实以此为目标。

当我们面临框架选择时,尽量不要草率地做出决定。在全身心投入之前,应该首先看看是否可以部分地采用以增加了解。另外,请尽可能长时间地将框架留在架构边界之外,越久越好。

7.个人总结

  1. 软件架构的终极目标是用最少的人力成本即可满足构建和维护该系统的需求。
  2. 有没有什么办法判断软件架构设计的好不好么?如果一个系统每次发布新版本都会提升下一次变更的成本,这个系统的架构设计就是不好的!比如说随着产品的发布,产品的工程师人数直线上涨,但是系统代码生产效率却没有得到相应的提高。
  3. 如果忽视软件架构的价值,系统将会变得越来越难以维护,终会有一天,系统将会变得再也无法修改。如果系统变成了这个样子,那么说明软件开发团队没有和需求方做足够的抗争,没有完成自己应尽的职责。
  4. 编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构。直到今天,我们也一共只有三个编程范式,而且未来几乎不可能再出现新的。并且,这三个编程范式都是在 1958 年到 1968 年这 10 年间被提出来的。
    • 结构化编程
    • 面向对象编程
    • 函数式编程
  5. SOLID 原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。
    • 单一职责原则(SRP): SRP 的最终描述:任何一个模块都应该只对某一类行为负责。 (我觉得这种描述最好!最能体现 SRP 具体要表达的意思。)
    • 开闭原则(OCP): 开闭原则(OCP)是 Bertrand Meyer 在 1988 年提出的,该设计原则认为: 设计良好的计算机软件应该易于扩展,同时抗拒修改。
    • 里式替换原则(LSP): 该设计原则是 Barbara Liskov 在 1988 年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
    • 接口隔离原则(ISP): 任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。
    • 依赖反转原则(DIP) : 如果要设计一个灵活的系统,在源代码层级的依赖关系中就应该多应用抽象类型,而非具体实现。
  6. 组件构建原则 :
    • 组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最.小实体。例如,对于 Java 来说,它的组件是 jar 文件。我们可以将多个组件链接成一个独立可执行文件,也可以将它们汇总成类似.war 文件这样的部署单元,又或者,组件也可以被打包成.jar、.d11 或者.exe 文件,并以可动态加载的插件形式来独立部署。但无论采用哪种部署形式,设计良好的组件都应该永远保持可被独立部署的特性,这同时也意味着这些组件应该可以被单独开发。
    • 3 个与构建组件相关的基本原则:
      • REP :复用/发布等同原则。组件中的类与模块必须是紧密相关的。也就是说,一个组件不能有一组毫无关联的类和模块组成,它们之间应该又一个共同的主题或者大方向。
      • CCP :共用闭包原则。我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。
      • CRP : 共同复用原则。不要强迫一个组件依赖它不需要的东西。
  7. 架构师本身必须是一线程序员,并且,他们往往是团队中能力最强的程序员。架构师会逐渐引导团队向一个能够最大化生产力的系统设计方向前进。软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
  8. 软件架构本身就是一门划分边界的艺术。边界的作用就是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、 可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,而这些其他组件要么是可以去掉的,要么是有多种实现的。
  9. 架构师应该花费很多精力来确保该架构的设计在满足用例需要的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。而且,良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、Web 服务以及其他与环境相关的工具。框架应该是一个可选项,良好的架构设计应该允许用户在项目后期再决定是否采用 Rails、Spring、Hibernate. Tomecat、MySQL 这些工具。同时,良好的架构设计还应该让我们很容易改变这些决定。总之,良好的架构设计应该只关注用例,并能将它们与其他的周边因素隔离。
  10. 测试并不是独立于整个系统之外的,恰恰相反,它们是系统的一个重要组成部.分。我们需要精心设计这些测试,才能让它们发挥验证系统稳定性和预防问题复发的作用。没有按系统组成部分来设计的测试代码,往往是非常脆弱且难以维护的。这种测试最后常常会被拋弃,因为它们终究会出问题。
  11. Kent Beck 描述了软件构建过程中的三个阶段:
    • “先让代码工作起来”一如果代码不能工作,就不能产生价值。
    • “ 然后再试图将它变好”一通过对代码进行重构, 让我们自已和其他人更好地理解代码,并能按照需求不断地修改代码。
    • “ 最后再试着让它运行得更快”一按照性 能提升的“需求”来重构代码。

更新: 2022-02-18 13:13:30
原文: https://www.yuque.com/snailclimb/to3hqu/muhryu

Java 后端面试知识库