软件架构整洁之道-读书笔记(1)

《软件架构整洁之道》是绝对的好书,是真正的顶级程序员编写的关于架构设计的书,一口气读完,有酣畅淋漓之感,解除了自己长久以来非常多的疑惑。这么好的书,这么多年才发现,简直是犯罪。

这三篇笔记几乎就是原书的摘要,因为原文写得足够精彩,无需我补充什么;笔记的内容结构与原书也完全一致,之所以分成三篇,纯粹是篇幅原因。

第一部分:绪论

第一章,概述

1、软件架构和设计有什么区别?

通常情况下,”架构“这个词往往使用于“高层级“的讨论中,这些讨论暂时将”底层“实现细节排除在外;而”设计“一词往往用来指代具体的系统底层组织结构和实现细节。

但是从一个真正的系统架构师的日常工作来看,这样的区分是根本不成立的。以建筑设计师要做的事情为例,房屋架构当然应该包括房屋的形状、外观设计、高度、房间布局;但如果仔细查看建筑师的图纸,会发现其中也充斥着大量的细节,譬如我们可以看到每个插座、开关以及每个点灯具体的安装位置,对排水设施、墙体、屋顶和地基也有非常详细的说明。总体来说,架构图里实际包含了所有的底层设计细节,这些细节共同支撑了顶层的架构设计,底层设计信息和顶层架构设计共同组成了整个房屋的架构文档。

软件设计也是如此:底层细节和高层架构信息是不可分割的,他们组合在一起,共同定义了整个软件系统,缺一不可。所谓底层和高层就是一系列决策组成的连续体,并没有清晰的分界线。

这里我有两个结论:1、软件架构设计必须要涵盖底层技术;2、架构师必须要对所有涉及的底层技术有足够的理解和掌握。

2、架构的目标是什么?

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

一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在软件的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。就这么简单。

架构目标是长期目标,要基于软件生命周期来衡量。另一方面,架构的价值体现,并不需要很“长期”。

3、程序员的认知问题

绝大多数程序员并不懒惰,却总是过于自信:持续地低估那些不良设计与代码的破坏性。他们普遍用一句话来欺骗自己:”我们可以未来重构代码,产品上线最重要!“。但是结果大家都知道,市场的压力永远不会消退,产品上线以后,重构工作就再没人提起,或者一再被提起却很难付诸行动。

"先上线,未来再重构”的想法本身是一种悖论,把“重构”说成了是一件很简单的事情似的。事实上,设计不良的系统是很难修改的,相比增加新功能,重构代码一样或更加困难。更不要说,大规模重构线上系统所带来的精神压力。

接下来,程序员可能走向另一个极端,认为挽救一个系统的唯一办法是抛弃现有系统,设计一个全新的系统来替代。这是另一种盲目自信,试问:如果是程序员的认知导致了目前的困境,我们有什么理由认为重新来一遍结果会更好呢。

一个较大的系统,如果你没有能力重构它,那么何来自信重写它?

第二章:软件架构的两个价值维度

每个软件系统,我们都可以通过行为和架构两个维度来体现它的价值。软件开发人员,应该确保自己的系统在这两个维度上的实际价值都能长时间保持在很高的状态。不幸的是,他们往往只关注一个维度,导致系统的价值最终趋降为0。

1、行为价值

软件的行为价值是最直观的价值维度。程序的工作直接目标就是,给系统的使用者提供价值,为此程序员需要为使用者编写一个对系统功能的定义,也就是需求文档。然后再把需求文档转化为实际的代码。

因此,大部分程序员认为他们的全部工作是:按照需求文档编写代码,并修复任何Bug。这真是大错特错。

2、架构价值

软件系统的第二个价值维度,就体现在软件这个单词上:software,soft的意思不言而喻,是指软件的灵活性。为了达到软件的本来目的,软件系统必须足够软,也就是应该容易被修改。当需求方改变需求是,随之所需的软件变更必须可以简单而方便地实现。变更实施的难度应该和变更的范畴成等比关系,而与具体的变更形状无关。

从需求方的角度来看,他们所提出的一系列变更需求的范畴都是类似的,因此成本也应该是固定的。但从开发者的角度来看,随着项目规模的增大,变更越来越难,因为现有系统的形状永远和需求的形状不一样。

问题的根源当然就是系统的架构设计,如果系统的架构设计偏向某种特定的形状,那么新的变更就会越来越难以实现。

这里揭示出:软件的架构价值,在于降低变更的成本,而不在于能否实现当前所需的功能。

3、哪个维度的价值更重要

那么,究竟是系统行为更重要,还是系统架构的灵活性更重要?如果这个问题由业务部门来回答,答案通常是系统正常工作更重要。而开发人员也就更随着采取这种态度,但这种态度是错误的。

这里的逻辑是这样的:如果一个程序可以正常工作,但是无法修改,那么当需求变更发生时,它就不能正常工作了,而且我们无法通过修改让它正常工作,因此这个程序的价值将归零。反过来,如果某个程序目前无法正常工作,但是我们很容易地修改它,并且可以随着需求变化不停地修改它,那么这个程序会持续产生价值。

或许,你认为:没有什么程序是不可以修改的。但是现实中,确实存在一些系统,实施修改的成本和风险远远超过了变更带来的价值。这种程序在就成了事实上不可修改的程序。

再回过头,来推演一下业务部门的价值逻辑,由于他们不懂架构,自然认为完成现在功能远比实现未来的灵活度更重要。但讽刺的是,如果将来业务部门提出一项需求,而你评估的工作量大大超出了他们的预期,这帮家伙一定喷你一脸。

4、开发者的责任

按“重要性—紧急度”二维矩阵来划分,系统的架构价值应该落入“重要—不紧急”区间,而系统的行为价值则分布于“重要—紧急“和”不重要—紧急“两个区间。业务需求嘛,就没有不紧急的,这完全可以理解;开发者和业务人员经常犯的共同错误是,将”重要—不紧急“的需求提升到”重要—紧急“区间,进而挤压了”重要—不紧急“的架构工作。

业务人员本来就没有能力评估系统架构的重要程度,这是开发者的责任。软件系统的可维护性需要开发者来保护,这是开发者角色的一部分,也是开发者职责中不可缺少的一部分。研发人员必须从公司长远利益触发,与其他部门抗争,维护系统的架构价值。

第二部分:从基础构件开始:编程范式

第三章:编程范式总览

编程范式是指程序的编写模式,与具体的编程语言无关。这些范式告诉你应该在什么时候采用什么样的代码结构。我们有三个编程范式,它们都在1958~1968这十年间出现,而且未来几乎不可能再出现新的。

1、结构化编程

结构化编程是第一个被普遍采用的编程范式(不是第一个被提出的),由DijKstra于1968年最先提出。他论证了goto这样无限制的跳转语句伤害程序的整体结构,也是他最先主张我们使用现在熟知的if/then/else语句和do/while/until语句来代替goto。

结构化编程范式归结为一句话:对程序控制权的直接转移进行了限制和规范。

2、面向对象编程

第二个被广泛采用的编程范式,1966年被提出(注意:比结构化范式早)。它的本质是:对程序控制权的间接转移进行了限制和规范(限制了函数指针的使用)。

###3、函数式编程

尽管函数式编程近些年才开始流行,但它其实是最早被发明的。是又和阿兰图灵同时代的数学家Alonzo Churche在1963年发明的λ演算衍生而来。1958年John Mccarthy利用它作为基础发明了LISP语言。

λ演算的一个核心思想就是不可变性:某个符号对应的值是永远不变的,所以函数式语言在理论上没有复制语句的。因此函数式编程对程序的复制进行了规范。

小结

以上三种编程范式都是在限制和规范程序员的能力,没有一个范式是增加新能力的。结构化编程限制了goto语句,面向对象编程限制了函数指针,函数式编程限制了赋值语句。还有什么可限制的吗?没有了,所以不会有新的编程范式了。

编程范式通过束缚程序员的手脚,赋予了程序员构建大规模系统的能力!!!

第四章:结构化编程

1、如何证明程序的正确性:数学推导

DijKstra很早就得出结论:编程是一项难度很大的活动,一段程序无论复杂与否,都包含了很多的细节信息,如果没有工具的帮助,这些细节信息是远远超过一个程序员的认知能力范围的。DijKstra提出的解决方案是采用数据推导方法:原理是类似数学归纳法,程序员可以用代码将一些已经证明可用的结构串联起来,只要自行证明这些额外的代码是正确的的,就可以推导出整个程序的正确性。

2、Goto有害

在研究过程中发现,Goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元;Goto语言的另一些用法虽然不会导致这种问题,但可以用更简单的if-then-else、do-while替代。因此只要摒弃Goto语言,将程序的控制机制限制为顺序、分支、循环三种,则一定可以将程序分解为更小的、可证明的单元。就这样,结构化编程诞生了。

3、功能性拆分

结构化编程范式可以将一个大型问题拆分为一系列高级函数的组合,而这些高级函数各自又可以继续被拆分为一系列低级函数,如此无限递归。而且,每个被拆分出来的函数也可以用同样的范式来书写。

正是采用这种技巧,程序员得以将大型系统拆分为模块和组件,而后者可以拆分为更小的、可证明的函数。

时至今日,在架构设计领域,功能性拆分仍然是解决问题的首要策略。《Unix编程艺术》一书中提到:编写复杂程序的唯一方法,是将它拆分为简单的子程序。

4、形式化证明没有发生

不过没有人会为每个函数书写冗长复杂的正确性推导过程,也没有几个程序员会认为形式化验证是产出高质量软件的必备条件。

软件开发更像一门科学研究学科,而不是数学研究过程。科学研究的特征是,对于一个结论,我们不需要证明它,只需要它无法被证伪就行。比如,我们无法推导出万有引力公式,但并不妨碍我们认可它的正确性,因为我们无法找到一个违背它的案例。对一段程序而言,如果我们运行足够的测试仍然不能证明程序有错,那么可以认为,在当前环境下,程序已经足够正确。

5、测试通过尝试证伪来保证程序正确性

最终,软件测试而不是数学推导成为保证软件正确性的常规手段。结构化编程范式促使我们将一段程序递归降解为一系列可证明的小函数,然后再编写相关的测试来证明这些函数是错误的。如果这些测试无法证伪这些函数,那么我们可以认为这些函数是足够正确的,进而推导整个程序是正确的。

第5章:面向对象编程

一个首要的问题是:究竟什么是面向对象?

1.面向对象编程的来历

为了回答这个问题,我们先回顾一下面向对象编程的创建历史。它于1966年,由Ole Johan Dahl和Kriste Nygaard总结归纳出来。这两个程序员注意到ALGOL语言中,函数调用栈可以被迁移到堆内存区域,这样函数定义的局部变量,就可以在函数返回之后继续存在。顺理成章地,这个函数成为了一个构造函数,而它所定义的本地变量就是类的成员变量;构造函数定义的嵌套函数就成为了成员方法。

2.面向对象是数据与函数的组合?

因此,那种认为面向对象是“数据与函数的组合”的说法并不贴切,因为它似乎暗示了o.f()与f(o)之间有本质的不同。从面向对象的来历我们知道,这不是事实。

o.f()和f(o)只不过是文法形式上的差别而已。

3.面向对象是一种建模方式?

另一种看法:“面向对象编程是一种对真实世界进行建模的方式”,这种回答只能算避重就轻。任何编程方法都在对现实世界进行建模,或许我们认为“面向对象是一种更好的建模方式,对象与现实世界的概念关系更紧密,因而使得软件开发更容易”。不过这并没有在编程范式这个层面道出:面向对象编程究竟是什么?

4、面向对象是封装、继承、多态这几项特性的有机组合?

  • 封装

封装经常被作为面向对象定义的一部分,不过它并不是面向对象所独有。C语言的封装可以做的更好:头文件定义类型、函数的原型,类型的结构和函数实现可以完全隐藏在静态库里面。而C++要求类的成员变量必须声明在头文件中,反而破坏了封装性;java则更进一步,破坏地更厉害。

大家可以想象,如果从隐藏细节和安全的角度来评判,有比操作系统的系统调用(System Call)更好的封装吗?它和面向对象可没半毛钱关系。

  • 继承

继承的主要作用是让我们可以在某个作用域内对外部定义的某一组变量或函数进行重用&覆盖。在C语言里,通过函数指针、struct字段对齐,完全可以达到类似的效果。不过确实,面向对象的继承使得这个机制更加规范和安全。因此,继承虽然算不上特别大的创新,确实带来了一些便利。

有一种观点:继承并非面向对象的必须特性,比如go语言,完全不支持继承,成功地用”组合“取而代之。不过Go语言的”组合“是强化过的,支持对现有类型的字段&成员方法进行覆盖,它本质上是传统继承机制的弱化版本。因此,我认为,继承机制对面向对象语言来说是不可或缺的,但我们未必需要java&C++语言那种完整、严谨、繁琐的实现方式。

  • 多态

多态不是面向对象的创新,linux的IO库和文件系统实现,早就将多态机制发挥得淋漓尽致,只不过它们是用函数指针来实现而已。多态的强大之处是实现设计上的”依赖反转“:高层模块定义接口,底层机制实现接口,代码的运行时调用关系(高层调用底层实现)和源码依赖关系(底层依赖高层接口)是相反的。这样一来,高层策略和底层机制可以相互独立开发、部署。

与继承类似,面向对象的多态机制更加安全便利,让程序员远离函数指针这种危险品。

5、面向对象到底是什么?

这个问题在业界很有争议,上面的讨论也没能给出一个确切的答案。但对一个软件架构师来说,要明白面向对象的力量所在:通过多态手段对源代码中的依赖关系进行控制,可以构建出某种插件式架构,让高层策略组件与底层实现组件相分离。

而在编程范式层面,面向对象限制了对函数指针的使用,用多态取而代之。

第六章:函数式编程

1、函数式编程与不可变性

函数式编程阻止程序员使用变量(准确说,是可变变量),函数就像流水线上的处理节点,一旦流水线运转起来,就能自动地将输入数据转换为输出数据,不需要程序员使用可变变量来维护中间状态。

为什么这种”不可变性“是有意义的?答案显而易见:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那么就不可能产生竞争或者并发更新问题。

2、不可变性是否可行

作为架构师,我们要问的问题是:不可变性是否实际可行?如果我们能忽略处理器与存储器的限制,那么答案是肯定的,否则不可变性只有在一定情况下可行。

最近几年大火的区块链项目,就是这种不可变性的运用。它不去修改旧的数据状态,而是将修改记录作为节点Append到链条末尾(这比修改原始数据更加安全、可靠),通过对链条上的节点进行溯源,我们可以计算出当前数据状态。

3、可变组件与不可变组件

一种常见的方式是将应用程序切分为可变的和不可变的两种组件,不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变组件最终通过与某个可变组件通信来修改状态。

而可变组件通常采用某种事务形的内存来保护可变变量,避免同步和竞争问题。

显然,可变组件的编写、维护、调试难度更大;架构师应该着力将大部分处理逻辑归于不可变组件中,可变组件中的逻辑越少越好。

编程范式总结

  • 结构化编程是对程序控制权的直接转移的限制(去除goto语句,将程序结构限定为顺序、分支、循环三种,可以再加上子程序);
  • 面向对象编程是对程序控制权的间接转移的限制(去除函数指针,代之以多态);
  • 函数式编程是对程序中赋值操作的限制(限制可变变量的使用)。

这三个编程范式都对程序员提出了新的限制,每个范式都在约束某种编写代码的方式,没有一个编程范式是在增加新的能力。也就说,过去50多年,我们学到的东西主要是——什么不应该做。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值