软件设计的哲学-读书笔记

复杂性的成因

  1. 依赖:
    1. 依赖关系过多、复杂
    2. 表现
      1. 放大变化
      2. 认知负担
  2. 模糊:
    1. 重要信息不明显
    2. 不一致
    3. 表现
      1. 未知

3 Working Code Isn’t Enough

保证代码运行是不够的。


战术编程 VS 战略编程

投资思维

  1. 为每个新类寻找简单的设计;
  2. 撰写好的文档;
  3. 修复之前设计的不合理处;
  4. 现在的10-20%的总开发时间 > 以后的20%+的减慢开发速度;

4 Modules Should Be Deep

模块应该足够深。


4.1 Modular design

模块可以采取许多形式,例如类、子系统或服务。

模块设计的目标是尽量减少模块之间的依赖关系。

好的模块接口比实现更简单。


4.2 What’s in an interface?

每个接口包括非正式元素,不是编程语言强制要求的,
使用上的约束,例如方法调用的隐含顺序。


4.3 Abstractions

抽象是对一个实体的简化视图。

抽象比必要更复杂

  1. 包括了不真正重要的细节,增加了使用抽象的开发人员的认知负荷;
  2. 省略了真正重要的细节时,导致了晦涩难懂,开发人员需要去看实现细节;

设计抽象的关键是了解什么是重要的,并寻找将重要信息量减至最少的设计。


4.4 Deep modules

最好的模块

  1. 具有简单的接口;
  2. 提供强大的功能;

模块的深度

  1. 简单接口,成本低,宽度
  2. 功能强大,收益高,深度
  3. 好的模块——》深的模块

深的模块示例

  1. Unix/Linux系统调用I/O;
  2. 高级语言中的垃圾回收器,例如Go或者Java;

4.5 Shallow modules

浅的模块示例

  1. 链表;

浅层类有时是不可避免的,但它们在管理复杂性方面没有多大帮助,有时甚至会增加复杂度。

  1. 浅层模块提供的好处被学习和使用其接口的成本所抵消;
  2. 小模块往往是浅层的。

4.6 Classitis

传统的鼓励导致大量的浅层类和方法,这增加了整个系统的复杂性。

  1. 小类;
  2. 小方法;

4.7 Examples: Java and Unix I/O

FileInputStream, BufferedInputStream, ObjectInputStream,过于复杂。

接口使用的常见情形应该被设计得尽可能简单。

如果一个接口有许多功能,但大多数开发人员只需要了解其中一些功能,则该接口的有效复杂性只是常用功能的复杂性。


4.8 Conclusion

关于接口的非正式部分,作者目前的观点:用英语描述的文档 > 形式化规范语言编写。


5 Information Hiding (and Leakage)

信息隐藏(和泄漏)

第5章 ~ 第9章在讨论创建深模块的技术


5.1 Information hiding

每个模块应该封装一些知识,这些知识代表设计决策。

  1. 简化了模块的接口,隐藏了实现细节,减少了认知负担;
  2. 更易于模块的演化;

5.2 Information leakage

信息泄漏

  1. 原因:一个设计决策反映在多个模块中;

  2. 影响:在模块之间创建依赖关系;

  3. 后门泄漏更糟,当接口的实现变化时,接口的定义也要变化;

  4. 解决方案

    1. 从所有受影响的类中提取信息,并创建一个封装这些信息的新类;
    2. 简化接口,隐藏暴露的不必要知识;

5.3 Temporal decomposition

时间分解

  1. 原因:系统结构对应于操作将发生的时间顺序;
  2. 影响:系统结构的信息泄露;
  3. 解决方案
    1. 专注于执行每个任务所需要的知识,而不是任务发生的顺序;
    2. 将操作封装为一个单一的类,在不同的过程中使用;

5.4 Example: HTTP server

5.5 Example: too many classes

HTTP 请求中请求


7 Different Layer, Different Abstraction

7.1 Pass-through methods

传递方法使类更浅:它们增加接口。

  1. 实际使用的例外:converter和builder
  2. 职责存在重叠,如图(a)所示,其重构解决方案
    1. 将较低级别类暴露给较高级别类的调用者,从较高级别类中删除所有对功能的责任,如图(b)所示;
    2. 重新分配类之间的功能,如图(c)所示;
    3. 如果类无法解耦,将它们合并,如图 (d)所示;
重叠职责的重构方案

7.2 When is interface duplication OK?

相同的接口,不同实现,减少了认知负荷。


7.5 Pass-through variables

引入上下文,传递对象。

引入上下文对象

避免问题的最好方法是使上下文中的变量为不可变的。


8 Pull Complexity Downwards

对于一个模块来说,拥有一个简单接口比拥有一个简单实现更重要。

向下拉取复杂性,最小化整个系统的复杂性。

  1. 被拉取的复杂性与类现有的功能密切相关;
  2. 向下拉取复杂性将在应用程序的其他地方导致许多简化;
  3. 向下拉取复杂性简化了类的接口;

在开发模块时,寻找机会让自己承受一点额外的痛苦,以减少用户的痛苦。


9 Better Together Or Better Apart?

合并的场景

  1. 共享信息;
  2. 简化接口;
  3. 消除重复;

分开特殊用途代码与通用代码

  1. 特殊用途代码向上移动到较高层;
  2. 通用代码向下移动到较低层;
是否分开方法

决定是否拆分或合并模块应该基于复杂性。选择导致最佳信息隐藏、最少依赖关系和最深接口的结构。


10 Define Errors Out Of Existence

定义错误不存在:在许多情况下,操作语义可以修改,以便正常行为处理所有情况,并且没有异常情况需要报告。

减少异常处理器数量

  1. 定义错误不存在,减少错误的最佳方法是使软件更简单;
  2. 屏蔽异常,不需要软件的较高级别感知;
  3. 异常聚合,使用单个代码处理许多异常,用单一通用机制来替换几个专为特定情况定制的机制;
  4. 程序崩溃,打印诊断信息,终止应用程序;
    1. 检测到内存耗尽时,立即崩溃;
    2. 复制存储系统的核心价值是恢复丢失的数据,在 I/O 错误上终止不合适;

设计特殊情况不存在:通过设计正常情况,以一种自动处理特殊情况而不需要任何额外代码的方式。

  1. 异常是特殊情况代码的最重要来源之一;
  2. “定义错误不存在”属于“设计特殊情况不存在”;

处理异常的哲理;

  • 暴露重要的内容,隐藏不重要的内容。

11 Design it Twice

考虑每个主要设计决策的多个选择,处于一个更好的位置设计方案。

  1. 不需要确定每个替代方案的每个功能。在这个阶段,勾勒出几个最重要的方法;
  2. 挑选截然不同的方法。即使确信只有一种合理的方法,认为第二种设计很糟糕;

大型软件系统的设计:

  1. 本身问题过于复杂;
  2. 没有人足够好;
  3. 因此想获得真正伟大的结果,必须要考虑多个可能性;

设计多次

  1. 专注于伟大的设计需要坚持练习;
  2. 系统设计上存在最佳实践;

12 Why Write Comments? The Four Excuses

例如选择变量名称以及如何使用文档来改进系统的设计

  1. 好的注释可以对软件的整体质量产生重大影响;
  2. 编写好的注释并不难;
  3. 编写注释实际上可以很有趣;

不写注释的理由

  1. 好的代码是自我说明的;
    1. 仍然有大量的设计信息无法在代码中表达,例如
      1. 对每个方法做什么或其结果的高层次描述;
      2. 特定设计决策的理据;
      3. 在什么情况下调用特定方法是有意义的;
    2. 对于大型系统,用户阅读代码来学习行为是不实际的;
    3. 注释是抽象的基础,不如代码精准,但可以提供更多的表达能力;
  2. 我没有时间写注释;
    1. 如果你允许注释被降级,你最终将没有任何注释;
    2. 必须在前期花一些额外的时间来创建一个干净整洁的软件结构,这将使你能够长期有效地工作;
    3. 写注释不需要花太多时间,不会超过开发时间的10%;
    4. 许多最重要的评论是那些与抽象相关的评论,例如类的顶级文档和方法。
  3. 注释过时了,变得误导性;
    1. 只有在代码发生重大变化时,才需要对文档进行重大更改,而代码的更改将需要比文档更改更多的时间;
    2. 保持文档、注释与代码的联系;
    3. 代码审查时,检测和修复过时的注释;
  4. 我见过的注释都是毫无价值的,为什么要费心呢?
    1. 大多数文档和注释提供的有效信息较少;
    2. 一旦你知道如何写,写好文档并维护并不难;

13 Comments Should Describe Things that Aren’t Obvious from the Code

注释的指导原则:注释描述代码无法明显体现的内容。

注释补充抽象信息的描述。


好的注释以比代码更抽象/更详细的细节级别解释事情。

  1. 注释的惯例,例如Java的Javadoc;
  2. 每个类都应该有一个接口注释,描述该类提供的总体抽象;
  3. 每个方法都应该有一个接口注释,描述其总体行为、其参数和返回值、它产生的任何副作用或异常,以及调用该方法之前调用者必须满足的任何其他要求;

不要重复代码

  1. 通过注释可以写代码,对代码没有理解作用;
  2. 在注释中使用与被注释实体名称相同的单词;

添加注释,跟学习的程度相关

  1. 低级别的注释增加精确度;
    1. 这个变量的单位是什么?
    2. 边界条件是包含性的还是排他性的?
    3. 如果允许空值,它意味着什么?
    4. 如果一个变量引用一个最终必须被释放或关闭的资源,谁负责释放或关闭它?
    5. 对于这个变量,是否有某些属性始终为真(不变量),例如“这个列表始终至少包含一个条目”?
    6. 关注变量代表什么,而不是它是如何被操纵的。
  2. 高级别的注释增强直觉;
    1. 这段代码在尝试做什么?
    2. 你能说的最简单的事情是什么,可以解释代码中的所有内容?
    3. 这段代码最重要的事情是什么?
    4. 触发代码调用的情形,尤其是对于特殊情形。

接口文档:开发人员使用类必须知道的信息。

接口实现注释:什么和为什么,而不是如何。


14 Choosing Names

就像大多数错误一样,一旦你弄清楚了,问题变得十分简单。

名称是复杂实体的简单抽象。

好的名字

  1. 精确性
    1. a GUI text editor, x—>charIndex and y—>lineIndex
    2. boolean blinkStatus = true—>boolean cursorVisible = true
    3. VOTED_FOR_SENTINEL_VALUE = "null"—>NOT_YET_VOTED = "null"
    4. Red Flag: Vague Name,模糊的名称
      1. 为特定变量想出一个精确、直观、且不太长的名称;
      2. 变量的使用范围限定且变量的含义在代码中易见,可以不需要一个长名称;
      3. 广泛的功能需要更通用的参数名称;
      4. 试图使用一个变量来表示几件事——》分开到多个变量中;
    5. Red Flag: Hard to Pick Name,难以挑选名称
      1. 可能底层对象没有一个干净的设计;
  2. 一致性
    1. 始终使用公共名称用于给定的目的;
    2. 除了给定的目的,永远不要使用公共名称用于任何事情;
    3. 确保目的足够狭窄,以至于所有具有该名称的变量具有相同的行为;
    4. srcFileBlock and dstFileBlock;
    5. 在最外层的循环中始终使用 i,在嵌套的循环中始终使用 j。

可读性必须由读者,而不是由作者来决定。

一个名字的声明和它的使用之间的距离越大,这个名字应该越长。


15 Write The Comments First

使用注释作为设计过程的一部分。

编写注释的最佳时间是在过程开始时,因为您正在编写代码。

  1. 先写注释;

16 Modifying Existing Code

维护注释

  1. 将注释放在描述的代码附近;
    1. 一个注释离它所描述的代码越远,它就应该越抽象;
    2. 注释属于代码,而不是提交日志;
  2. 避免重复;
    1. 不要在另一个模块中重新记录一个模块的设计决策;
      1. 不要在方法调用之前放置注释,以解释在调用的方法中发生了什么;
    2. 如果信息已经记录在程序之外的地方,就不要在程序中重复文档;
      1. 只需参考外部文档;
  3. 检查差异;
    1. 花几分钟时间扫描整体更改;
    2. 确保每个更改都正确反映在文档中;
  4. 更高层次的注释更容易维护;

17 Consistency

一致性的例子

  1. 命名;
  2. 编码风格;
  3. 接口;
  4. 设计模式;
  5. 不变量;

确保一致性

  1. 文档;
    1. 更本地化的约定,在代码特定位置指定;
  2. 强制执行;
    1. 脚本;
    2. 代码审查;
  3. 遵循惯例;
    1. 不要改变现有的惯例。抵制“改善”现有惯例的冲动;
    2. 有一个“更好的想法”并不是引入不一致性的充分理由;
      1. 是否有重要的新信息证明你的方法是合理的,而旧惯例建立时没有这些信息?
      2. 新方法是否好得多,以至于值得花时间更新所有旧用法?
      3. 继续升级;
        1. 当你完成时,应该没有旧惯例的任何痕迹;
        2. 仍然存在其他开发人员不知道新惯例的风险,因此他们可能在将来重新引入旧方法。
    3. 总体而言,重新考虑既定的惯例很少能很好地利用开发人员的时间。

一致性只有在开发人员有信心“如果它看起来像一个x,那么它确实是一个x”时才能提供好处。


18 Code Should be Obvious

确定代码是否显而易见的最佳方式是通过代码审查。

使代码更明显

  1. 精确有意义的名称;
  2. 一致性;
  3. 明智地使用空格;
  4. 注释;

使代码不明显

  1. 事件驱动编程;

软件应该易于读,而不是易于写。

如果代码符合读者将期望的惯例,那么代码是最明显的;
如果它不符合,那么记录行为是很重要的,这样读者就不会感到困惑。

另一种思考明显性的方式是信息。
如果代码不明显,那通常意味着关于代码的重要信息读者没有。

为了使代码显而易见,你必须确保读者始终拥有他们需要的信息来理解它。

  1. 减少所需的信息量,使用设计技术,例如抽象和消除特殊情况;
  2. 利用读者已经在其他上下文中获得的信息,例如遵循惯例和符合预期;
  3. 在代码中呈现重要信息,例如好的名称和策略性注释。

19 Software Trends

过去几十年在软件开发中变得流行的趋势和模式,讨论其能否对抗软件的复杂性。

  1. 面向对象编程和继承
    1. 私有方法和变量可以用于确保信息隐藏;
    2. 接口继承,通过为多个目的重用相同的接口;
      1. 允许在解决一个问题时获得的知识,被用来解决其他问题;
      2. 为了使一个接口具有许多实现,必须捕获所有底层实现的基本功能,同时避开实现之间的细节;
    3. 实现继承,子类可以选择继承父类的方法实现,或者通过定义具有相同签名的新方法来覆盖它;
      1. 减少了随着系统演变而需要修改的代码量;
      2. 创建了父类和每个子类之间的依赖关系,使得修改层次结构中的某个类变得困难;
        1. 谨慎使用实现继承——》基于组合;
        2. 分来父类管理与子类管理的状态;
  2. 敏捷开发
    1. 开发是增量和迭代的;
    2. 可能会导致战术编程;
  3. 单元测试
    1. 测试应与开发紧密集成,程序员应为其自己的代码编写测试;
    2. 开发人员在没有良好测试套件的系统中避免重构。他们试图将每个新功能或错误修复的代码更改数量最小化,这意味着复杂性不断累积,设计错误得不到纠正;
  4. 测试驱动开发
    1. 在编写代码之前编写单元测试;
    2. 测试驱动开发关注让特定功能工作,而不是找到最佳设计;
    3. 在修复错误时,先编写测试代码(复现);
  5. 设计模式
    1. 设计模式代表了一种设计替代方案,解决了常见问题,提供了干净的解决方案;
    2. 过度应用的风险,设计模式是好的,并不意味着更多的设计模式是更好的;
  6. Getters and setters

每当遇到一个新的软件开发范式时,从复杂性的角度来挑战它:这个提议真的有助于最小化大型软件系统的复杂性吗?
DDD?


20 Designing for Performance

使用基本知识来选择设计替代方案,这些替代方案是“自然高效”的,但也是干净和简单的。

  1. 在进行任何更改之前,测量系统现有的行为。
    1. 测量将确定性能调优将产生最大影响的地方;
    2. 提供基准,可以重新测量性能;
  2. 设计围绕关键路径
    1. 根据经验,几乎总是有可能找到一个干净而简单的设计,非常接近理想;
    2. 特殊情况对性能并不那么重要,为特殊情况编写代码以简单性为重,而不是性能;

如果写干净、简单的代码,系统很可能足够快,以至于不必太担心性能。
在少数情况下,确实需要优化性能,关键仍然是简单:找到对性能最重要的关键路径,并使它们尽可能简单。


21 Conclusion

处理复杂性

  1. 导致复杂性的根本原因,例如依赖性和模糊性;
  2. 识别不必要复杂性的危险信号,例如信息泄漏,不需要的错误条件,或者名字太通用;
  3. 提出了一些创建更简单的软件系统的通用想法,例如努力实现深层次和通用的类,定义不存在的错误,以及将接口文档与实现文档分开;
  4. 讨论了生产简单设计所需的投资心态;

如何用最简单的结构来解决一个特定的问题?

  1. 一个既简单又强大的解决方案;
  2. 一个干净、简单、明显的设计;


提高设计技能,正向循环

  1. 更快地生产出高质量的软件;
  2. 软件开发过程更加有趣;

Summary of Design Principles

设计原则的总结,抽象的,偏理念。

  1. Complexity is incremental: you have to sweat the small stuff. (2.4 Complexity is incremental)
  2. Working code isn’t enough. (3 Working Code Isn’t Enough)
  3. Make continual small investments to improve system design. (3.3 How much to invest?)
  4. Modules should be deep. (4 Modules Should Be Deep)
  5. Interfaces should be designed to make the most common usage as simple aspossible. (4.7 Examples: Java and Unix I/O)
  6. It’s more important for a module to have a simple interface than a simple implementation. (8 Pull Complexity Downwards)
  7. General-purpose modules are deeper. (6 General-Purpose Modules are Deeper)
  8. Separate general-purpose and special-purpose code. (9.4 Separate general-purpose and special-purpose code)
  9. Different layers should have different abstractions. (7 Different Layer, Different Abstraction)
  10. Pull complexity downward. (8 Pull Complexity Downwards)
  11. Define errors (and special cases) out of existence. (10.9 Design special cases out of existence)
  12. Design it twice. (11 Design it Twice)
  13. Comments should describe things that are not obvious from the code. (13 Comments Should Describe Things that Aren’t Obvious from the Code)
  14. Software should be designed for ease of reading, not ease of writing. (Red Flag: Nonobvious Code)
  15. The increments of software development should be abstractions, not features. (19.2 Agile development)

Summary of Red Flags

危险信号表示系统设计出现了问题。

  1. Shallow Module: the interface for a class or method isn’t much simpler than its implementation. (4.5 Shallow modules)
  2. Information Leakage: a design decision is reflected in multiple modules. (5.2 Information leakage)
  3. Temporal Decomposition: the code structure is based on the order in which operations are executed, not on information hiding. (5.3 Temporal decomposition)
  4. Overexposure: An API forces callers to be aware of rarely used features in order to use commonly used features. (5.7 Example: defaults in HTTP responses)
  5. Pass-Through Method: a method does almost nothing except pass its arguments to another method with a similar signature. (7.1 Pass-through methods)
  6. Repetition: a nontrivial piece of code is repeated over and over. (9.4 Separate general-purpose and special-purpose code)
  7. Special-General Mixture: special-purpose code is not cleanly separated from general purpose code. (9.5 Example: insertion cursor and selection)
  8. Conjoined Methods: two methods have so many dependencies that its hard to understand the implementation of one without understanding the implementation of the other. (9.8 Splitting and joining methods)
  9. Comment Repeats Code: all of the information in a comment is immediately obvious from the code next to the comment. (13.2 Don’t repeat the code)
  10. Implementation Documentation ContaminantsInterface: an interface comment describes implementation details not needed by users of the thing being documented. (13.5 Interface documentation)
  11. Vague Name: the name of a variable or method is so imprecise that it doesn’t convey much useful information. (14.3 Names should be precise)
  12. Hard to Pick Name: it is difficult to come up with a precise and intuitive name for an entity. (14.3 Names should be precise)
  13. Hard to Describe: in order to be complete, the documentation for a variable or method must be long. (15.3 Comments are a design tool)
  14. Nonobvious Code: the behavior or meaning of a piece of code cannot be understood easily. (18.2 Things that make code less obvious)

posted @ 2024-08-27 09:56  夜是故乡明  阅读(4)  评论(0编辑  收藏  举报