《架构整洁之道》读书笔记(上)

第一章:整洁代码

1. 为什么会写糟糕的代码?

  想快点完成;赶时间;赶紧做完手上的工作好接着做下一件工作等。
  我们都曾经说过有朝一日再回头清理,当然在哪些日子里,我们都没有听过勒布朗法则:稍后等于永远不。

2. 糟糕代码的代价

  • 随着糟糕代码积累,进度延缓的情况会愈发严重。
  • 随着代码混乱的增加,团队生产力会持续下降,以至于趋向于零。

花时间保存代码整洁不但关乎效率,还关乎生存。

3. 对待代码态度

  你可曾遇到过某种严重到要花数个星期来做本来只需数小时即可完成的事的混乱状况?你可曾见过本来只需做一行修改,结果却涉及上百个模块的情况?这种事太常见了。怎样会发生这种事?理由有很多,但最终表现的是我们太不专业了。
  产品他们会奋力护卫进度和需求,这是他们该干的,我们则当以同等的热情护卫代码。

4. 什么是整洁代码

  作者列举了业界大佬对整洁代码的理解,每一个观点都值得好好体会。
  Bjarne Stroustrup观点:我喜欢优雅和高效的代码。代码逻辑应当直接了当,令陷阱难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱来。整洁代码只做好一件事。

提炼和补充:

  1. 整洁的代码读起来令人愉悦;
  2. 糟糕的代码引发混乱!别修改糟糕的代码时,往往会越改越烂;
  3. 完善错误处理代码,在细节上话心思;
  4. 整洁的代码只做好一件事,糟糕的代码想做太多事,它意图混乱,目的含混。

Grady Booch观点:整洁代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。

提炼和补充:

  1. 整洁的代码如同优美的散文;
  2. 代码应当讲述事实,不引人猜测。它只该包含必需之物;

  Dave Thomas观点:整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰的、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需的信息均可通过代码自身清晰表达。

提炼和补充:

  1. 易读和易修改;
  2. 整洁的代码在测试之上,没有测试的代码是不干净的;
  3. 推崇小块代码;
  4. 应当用人类可读的方式来写代码;

  Michael Feather观点:我可以列出我留意到的整洁代码的所有特点,他其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码的作者什么都想到了。如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。

提炼和补充:

  1. 整洁代码就是作者着力照料的代码;

  Ron Jeffries观点:近年来,我开始研究贝克的简单代码规则,差不多也都做琢磨透了。简单代码,依据重要顺序:

  1. 能通过所有的测试;
  2. 没有重复代码;
  3. 体现系统中的全部设计理念;
  4. 包含尽量少的实体,不如类、方法、函数等;

  Ward Cunningham观点:如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专门为解决那个问题而存在的,就可以称之为漂亮的代码。

第二章:有意义的命名

  1. 名副其实:选一个好名字要花时间,但省下来的时间不花掉的多。

注意命名,而且一旦发现有其他更好的名称,就换掉旧的。

  1. 避免误导:必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。
  2. 做有意义的区分:不要定义,没有提供正确信息,没有提供导向作者意图的线索的命名。
  3. 使用读得出来的名称。
  4. 使用可搜索的名称。
  5. 匈牙利语标法:
    早期是帮助记住类型,现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型,现在不建议使用,它增加了代码阅读的难度,它制造了让编码系统误导读者的可能性。
  6. 类名:类名和对象名应该是名词或名词短语。
  7. 方法名:方法应当是动词或动词短语。
  8. 每个概念对应一个词:给每个抽象概念造一个词,并且一以贯之。例如,使用fetch、retrieve 和 get 来给在多个类中的同种方法命名。

第三章——函数

  1. 短小:函数应该短小。
  2. 只有一件事:函数应该做一件事。做好这一件事。只有这一件事。
  3. 每个函数一个抽象层级:自顶向下读代码:向下规则。
  4. 使用具有描述性的名称:函数越短小,功能越集中,就越便于起个好名字。

起名字规则:

  1. 别害怕长名称;
  2. 别害怕花时间起名字;
  3. 命名方式要保存一致;
  1. 函数参数:最理想的参数是0(零参数函数),其次是1(单参数函数),再次是2(双参数函数),应尽量避免3(三参数函数)。
  2. 动词与关键词:给函数起一个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于单参数函数,函数和参数应当形成一种非常良好的动词/名称对形式。
  3. 别重复自己:重复可能是软件一起邪恶的根源。

第四章:注释

注释的恰当用法是弥补我们在用代码表达意图是遭遇的失败。

  1. 注释不能美化糟糕的代码
    写注释的常见动机之一是糟糕的代码的存在。带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎二复杂的代码像样得多。与其花时间编写解释你写的糟糕的代码的注释,不如花时间清理那堆糟糕的代码。

  2. 用代码来阐述
    用代码解释意图

  3. 好注释

  1. 法律信息
  2. 提供信息的注释
  3. 对意图的解释
  4. 阐释
  5. 警示

第五章——格式

你应该保持良好的代码格式,你应该选用一套管理代码格式的简单规则,然后贯彻这些规则。

  1. 格式的目的:格式关乎沟通,而沟通是专业开发者的头等大事。
  2. 垂直格式
  • 向报纸学习,名称应当简单且一目了然。
  • 概念间垂直方向上的区隔。
  • 垂直方向上的靠近,靠近的代码行暗示它们之间的紧密关系。
  • 垂直距离,关系密切的概念应该互相靠近。
  • 垂直顺序,自上向下展示函数调用依赖顺序,建立一种自顶向下贯穿代码模块的良好信息流。
  1. 横向格式
      水平方向上的区隔与靠近,用空格字符将彼此紧密相关的事物连接到一起,也用空格字符把相关性较弱的事物区隔开。水平对齐缩进.

  2. 团队规则
      每个程序员都有自己喜欢的格式规则,但如果在团队中工作,就是团队说了算。

第六章:对象和数据结构

对象暴露行为,隐藏数据,便于添加新对象类型而无须修改既有行为,同时难以在既有对象中添加新行为;数据结构暴露数据。没有明显的行为,便于向既有数据结构添加新的行为,同时难以向既有函数添加新的数据结构。

  1. 数据抽象:
      隐藏实现关乎抽象,暴露抽象接口,以便用户无须了解数据的实现就能操作数据本体。
  2. 得墨忒耳律:
      模块不应了解它所操作对象的内部情况。
  3. 数据传送对象:
      最为精炼的数据结构,是一个只有公共变量,没有函数的类。这种数据结构有时被称为数据传送对象。

第八章——边界

  整洁的边界,边界上的代码需要清晰的分割和定义了期望的测试。一个避免我们的代码过多地了解,第三方代码中的特定信息。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它的控制。

第九章:单元测试

  1. TDD三定律
  • 第一定律:在编写不能通过的单元测试前,不能编写生产代码;
  • 第二定律:只可编写刚好无法通过的单元测试,不能编译也算不通过;
  • 第三定律:只可编写刚好足以通过当当前失败的测试的生产代码;
  1. 保持测试整洁
    测试必须随生产代码的演进而修改。测试越脏,就越难修改。测试代码越缠结,你就越有可能花更多时间塞进新的测试,而不是编写新的生产代码。

  2. 整洁的测试
    整洁测试三要素:可读性、可读性和可读性。

第十章——类

  • 类型应该短小。
  • 单一权责原则:类或模块应该有且只有一条加以修改的理由。
    系统应该有许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达到期望的系统行为。
  • 内聚:类应该只有少数实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类中。如果一个类中的每个变量都被每个变量方法所使用,则该类具有最大的内聚性。

保存内聚性就会得到许多短小的类,将大函数拆分为许多小函数时,往往也是将类拆分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构

第十一章:DIP——依赖反转原则

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

  在应用DIP时,我们也不必考虑稳定的操作系统或者平台设施,因为这些系统接口很少会有变动。
  我们主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。

1. 稳定的抽象层

  我们每次修改抽象接口的时候,一定也会去修改对应的具体实现。但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。所以我们可以认为接口比实现更稳定。
  如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。下面,我们将该设计原则归结为以下几条具体的编码守则:

  • 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。这条守则适用于所有编程语言,无论静态类型语言还是动态类型语言。同时,对象的创建过程也应该受到严格限制,对此,我们通常会选择用抽象工厂(abstract factory)这个设计模式。
  • 不要在具体实现类上创建衍生类。上一条守则虽然也隐含了这层意思,但它还是值得被单独拿出来做一次详细声明。在静态类型的编程语言中,继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。即使是在稍微便于修改的动态类型语言中,这条守则也应该被认真考虑。
  • 不要覆盖(override)包含具体实现的函数。调用包含具体实现的函数通常就意味着引入了源代码级别的依赖。即使覆盖了这些函数,我们也无法消除这其中的依赖——这些函数继承了那些依赖关系。在这里,控制依赖关系的唯一办法,就是创建一个抽象函数,然后再为该函数提供多种具体实现。
  • 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。这基本上是DIP原则的另外一个表达方式。

2. 工厂模式

  如果想要遵守上述编码守则,我们就必须要对那些易变对象的创建过程做一些特殊处理,这样的谨慎是很有必要的,因为基本在所有的编程语言中,创建对象的操作都免不了需要在源代码层次上依赖对象的具体实现。
  在大部分面向对象编程语言中,人们都会选择用抽象工厂模式来解决这个源代码依赖的问题。

3. 小结

  随着内容的进一步深入,以及我们对高级系统架构理论的进一步讨论,DIP出现的频率将会越来越高。在系统架构图中,DIP通常是最显而易见的组织原则。

第十二章:组件

  组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。设计良好的组件都应该永远保持可被独立部署的特性,这同时也意味着这些组件应该可以被单独开发。

1. 组件发展史:

  1. 程序员可以完成掌握自己编写的程序所处的内存地址和存放格式,程序基本不能重定位。随着库函数增多,编译耗时越来越久,大型程序编译可能需要几个小时。
  2. 使用重定位技术:将函数名输出为元数据并存储起来,如果一段程序调用了某个库函数,编译器就会将这个函数的名字输出为外部引用(external reference),而将库函数的定义输出为外部定义(external definition)。加载器在加载完程序后,会将外部引用和外部定义链接(link)起来。这就是链接加载器(linking loader)的由来。
  3. 使用链接器:随着程序规模增多,链接加载器的加载过程越来越久(可能需要一个小时),程序员们只能将加载过程和链接过程也进行分离,他们将耗时较长的部分–链接部分-一放到了一个单独的程序中去进行,这个程序就是所谓的链接器(linker) 。链接器的输出是一个已经完成了外部链接的、可以重定位的二进制文件,这种文件可以由一个支持重定位的加载器迅速加载到内存中。这使得程序员可以用缓慢的链接器生产出可以很快进行多次加载的可执行文件。
  4. 随着硬件技术的提升,到了20世纪90年代中期,链接速度的提升速度已经远远超过了程序规模的增长速度。在大部分情况下,程序链接的时间已经降低到秒级。与此同时,编程领域中还诞生了Active-X、共享库、.jar文件等组件形式。由于计算与存储速度的大幅提高,我们又可以在加载过程中进行实时链接了,链接几个.jar文件或是共享库文件通常只需要几秒钟时间,由此,插件化架构也就随之诞生了。

2. 本章小结

  我们常常会在程序运行时插入某些动态链接文件,这些动态链接文件所使用的就是软件架构中的组件概念。在经历了50年的演进之后,组件化的插件式架构已经成为我们习以为常的软件构建形式了。

第十三章:组件聚合

  究竟是哪些类应该被组合成一个组件呢?在本章中,我们会具体讨论以下三个与构建组件相关的基本原则:

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

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

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

2. 共同闭包原则(CCP)

  我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。
  这其实是SRP原则在组件层面上的再度阐述。SRP原则:“一个类不应该同时存在着多个变更原因”,CCP原则也认为一个组件不应该同时存在着多个变更原因。
  CCP的主要作用就是提示我们要将所有可能会被一起修改的类集中在一处。也就是说,如果两个类紧密相关,不管是源代码层面还是抽象理念层面,永远都会一起被修改,那么它们就应该被归属为同一个组件。通过遵守这个原则,我们就可以有效地降低因软件发布、验证及部署所带来的工作压力。

3. 共同复用原则(CRP)

  不要强迫一个组件的用户依赖他们不需要的东西。

4. 组件聚合张力图

  上述三个原则之间彼此存在着竞争关系。REP和CCP原则是黏合性原则,它们会让组件变得更大,而CRP原则是排除性原则,它会尽量让组件变小。软件架构师的任务就是要在这三个原则中间进行取舍。
  下图,是一张组件聚合三大原则的张力图。图的边线所描述的是忽视对应原则的后果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8b9fZqn6-1656690214516)(76CC991CFCA742FA9CA03B59AE70F292)]

  • 只关注REP和CRP的软件架构师会发现,即使是简单的变更也会同时影响到许多组件。
  • 过于关注CCP和REP,则会导致很多不必要的发布。

  优秀的软件架构师应该能在上述三角张力区域中定位一个最适合目前研发团队状态的位置,同时也会根据时间不停调整。例如在项目早期, CCP原则会比REP原则更重要,因为在这一阶段研发速度比复用性更重要。
  般来说,一个软件项目的重心会从该三角区域的右侧开始,先期主要牺牲的是复用性。然后,随着项目逐渐成熟,其他项目会逐渐开始对其产生依赖,项目重心就会逐渐向该三角区域的左侧滑动。

5. 小结

  过去,我们对组件在构建过程中要遵循的组合原则的理解要比REP、 CCP、CRP这三个原则更有限。我们最初所理解的组合原则可能完全基于单一职责原则。然而,本章介绍的这三个原则为我们描述了一个更为复杂的决策过程。在决定将哪些类归为同一个组件时,必须要考虑到研发性与复用性之间的矛盾,并根据应用程序的需要来平衡这两个矛盾,这是一件很不容易的事。而且,这种平衡本身也在不断变化。也就是说,当下适用的分割方式可能明年就不再适用了。所以,组件的构成安排应随着项目重心的不同,以及研发性与复用性的不同而不断演化。

第十四章:组件耦合

1. 无依赖环原则

  组件依赖关系图中不应该出现环。
有两种解决方案:“每周构建” 和 “无依赖环原则(ADP)”

2. 每周构建

  存在的问题,随着项目越来越大,集成工作会越来越难以按时完成。

3. 消除循环依赖

  解决办法将研发项目划分为一些可单独发布的组件,这些组件可以交由单人或者某一组程序员来独立完成。当有人或团队完成某个组件的某个版本时,他们就会通过发布机制通知其他程序员,并给该组件打一个版本号,放入一个共享目录。这样一来,每个人都可以依赖于这些组件公开发布的版本来进行开发,而组件开发者则可以继续去修改自己的私有版本。
  每当一个组件发布新版本时,其他依赖这个组件的团队都可以自主决定是否立即采用新版本。若不采用,该团队可以选择继续使用旧版组件,直到他们准备好采用新版本为止。
  要求:必须控制好组件之间的依赖结构,绝对不能允许该结构中存在着循环依赖关系。

3.1 打破循环依赖

  有两种机制可以做到打破循环依赖。

  • 应用依赖反转原则(DIP)。
    看下面例子,将Entities 与 Authorizer 之间的依赖关系反转。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvndfgWI-1656690214517)(053304A9DEEF49B2BA32BD4773527929)]

  • 创建一个新的组件。
    存在的问题: 随着程序的不断演进,其组件结构也会不停地抖动和扩张。
    例子,让 Entities 与 Authorizer 共同依赖于一个新组件。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V7jBP3Fp-1656690214518)(9623E05730B74DECB44B3F216DE022B1)]

4. 自上而下的设计

  组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。
  组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图。这就是组件的依赖结构图不能在项目的开始阶段被设计出来的原因——当时该项目还没有任何被构建和维护的需要,自然也就不需要一张地图来指引。除此之外,我们还希望将项目变更所影响的范围被限制得越小越好,因此需要应用单一职责原则(SRP)和共同闭包原则(CCP)来将经常同时被变更的类聚合在一起。
  组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件。

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

5. 稳定依赖原则

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

6. 稳定抽象原则

  一个组件的抽象化程度应该与其稳定性保持一致。

7. 小结

  本章介绍了各种可用于依赖关系管理的指标,它们可以被用来量化分析某个系统设计与“优秀”设计模式之间的契合度。根据以往的经验,组件之间有些依赖关系是好的,有些依赖关系则是不好的,这些经验最后都会体现在这个设计模式中。当然,指标并不等同于真理,它只是对我们所定义标准的一个衡量。这些指标肯定是不完美的,但是我希望它们对读者有价值。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值