《架构整洁之道》1-6章笔记

513 阅读8分钟

感谢早早聊赠送的图书和淘宝妙净的亲笔签名。 image (3).png

前言

作者从1964年开始编程,到2016年已经超过50年,写过小型嵌入式系统、大型批处理系统、命令行、图形界面、进程管理、计费、通讯、设计工具、画图工具等。

领悟是:软件架构的规则是相同的

这么多千差万别的系统,软件架构规则和其他变量完全无关。今天的软件和过去的软件本质上仍然一样,都是有if语句、赋值语句、以及while循环组成。

50年来工具的质量提升了,编程语言稍微进步了一点,但计算机程序的基本构造没有发生什么变化。

软件架构的规则其实就是排列组合代码块儿的规则

概述

程序员将需求文档转换成可运行的代码并不难,难的是拥有一个好的架构,架构能力是需要经验和时间,并非所有人都愿意在架构上花时间学习。

某个系统因为其组件错综复杂,相互耦合紧密,而导致不管多小的改动都需要数周的恶战才能完成。

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

第一章 架构与设计究竟是什么

简短的说明:架构和设计一丁点去区别都没有。

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

接着作者举了几个例子来说明糟糕的设计是如何给公司代码人力成本的增长。

对系统的开发者来说,这会带来很大的挫败感,因为团队中并没有人偷懒,每个人还都是和之前一样在拼命工作。 然而,不管他们投入了多少个人时间,救了多少次火,加了多少次班,他们的 产出始终上不去。工程师的大部分时间都消耗在对现有系统的修修补补上,而不是真正完成实际的新功能。这些工程师真正的任务是:拆了东墙补西墙,周而往复,偶尔有精力能顺便实现一点小功能。

引用龟兔赛跑的故事:

  1. 慢但是稳,是成功的秘诀。
  2. 比赛并不是拼谁开始跑得快、谁更有力气。
  3. 心态越急,反而跑得越慢。

并提示我们:不要过于自信,不要持续低估那些好的、良好设计的、整洁的代码的重要性。要想跑的快,先要跑的稳当。

并且在结尾提出一个问题:如果挽救一个系统的办法是重新设计一个新的系统,那么,我们有什么理由认为从头开始,结果会更好呢?

第二章 两个价值维度

程序员并不应该把“按照需求文档编写代码,并修复bug”当做工作的全部。

提出的每个新需求好像一个不规则的拼图块,要在现有的拼图中插入这个拼图块,拼出一个新的形状。如果我们的架构设计偏向于维持整体形状,那么每次变更将非常艰难,好的架构设计应该做到与“形状”无关。

但在日常的开发工作中,架构设计工作并不会被放在它匹配的优先级上:

如果你问业务部门,是否想要能够变更需求,他们的回答一般是肯定的,而且他们会增加一句:完成现在的功能比实现未来的灵活度更重要。但讽刺的是,如果事后业务部门提出了一项需求,而你的预估工作量大大超出他们的预期,这帮家伙通常会对你放任系统混乱到无法变更的状态而勃然大怒。

所以,平衡系统架构的重性与功能的紧急程度这件事,是软件研发人员自己的职责

架构师的职责就是创建一个功能容易、修改简单、扩展轻松的架构,如果忽视架构的长期价值,一个系统变得难以维护,那么说明软件开发团队没有和需求放做足够的抗争,没有完成自己应尽的职责。

第三章 编程范式总览

结构化编程、面向对象编程、函数式编程三个编程范式都是在 1958 年到 1968 年这 10 年间被提出来的,后续再也没有新的编程范式出现过。

  1. 结构化编程对程序控制权的直接转移进行了限制和规范。
  2. 面向对象编程对程序控制权的间接转移进行了限制和规范。
  3. 函数式编程对程序中的赋值进行了限制和规范。

第四章 结构化编程

科学理论和科学定律的特点:它们可以被证伪,但是没有办法被证明

科学方法论不需要证明某条结论是正确的,值需要想办法证明它是错误的,如果某个结论经过一定的努力无法证伪,我们则任务它在当下是足够正确的。

Bohm 和 Jocopini 证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。

Dijkstra 认为程序员可以像数学家一样对自己的程序进行推理证明,可以用代码将一些己证明可用的结构串联起来,只要自行证明这外代码是正确的,就可以推导出整个程序的正确性。

最早的go to 语句的某些用法会导致某个模无法被递归拆分成更小的、可证明的单元。

结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。 这也是为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一

这让我明白了为什么说“测试覆盖率100%”和“不存在bug”之间关系的根源:

Dikstra 曾经说过“测试只能展示 Bug 的存在,并不能证明不存在 Bug”,换句话说,一段程序可以由一个测试来证明其错误性,但是却不能被证明是正确的。测试的作用是让我们得出某段程序已经足够实现当前目标这一结论。

第五章 面向对象编程

面向对象编程的3个特性:

  1. 封装可以把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数,数据则完全不可见。
  2. 继承的主要作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。
  3. 多态是同一个行为具有多个不同表现形式或形态的能力。

多态解决了依赖反转问题: 如实现一个播放器函数,要求可以播放音频和视频;音频、音频分别依赖自己的流接收、解码、播放函数,在没有多态概念时,要在播放函数中引入音频、视频各自的流接收、解码、播放等函数,换言之,这些底层的实现函数是这个播放器函数的强依赖。

有了面向对象的多态以后,视频、音频变成2个单独的解析器对象,播放器只需要根据不同的类型,区分调用视频的play方法,还是音频的paly方法,具体内部的流接收、解码等函数统统封装在它们内部,独立开发、独立部署。

以C语言为例,都可以实现这3种特性,面向对象编程并没有在封装、继承、多态开创出新或提出新的概念。但是面向对象编程语言提供了更便利、安全的使用这3种特性的能力

面向对象编程就是以多态为手段来对代码中依赖关系进行控制的能力。让架构师实现高层策略性组件与底层实现性组件相互分离,构建出插件式架构。

第六章 函数式编程

特点:函数式变成中的变量是不可变的。

可变性会导致很多问题,在一个复杂的系统中,如果底层函数能够轻松更改一个重要的公共变量,那将是一场灾难。

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

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

总结

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

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

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