[学习笔记] 架构整洁之道

前言:

学习笔记,随时更新。如有谬误,欢迎指正。


说明:

  1. 红色字体为较为重要部分。
  2. 绿色字体为个人理解部分。

推荐序一

  1. 所有的编程都在解决一个问题——分离控制和逻辑。控制就是业务无关的系统控制(多线程、异步等),逻辑就是业务逻辑。控制和逻辑构成整体软件的复杂度,有效的分离控制和逻辑会让系统得到最大简化。
  2. 要做到能正确的区分几组词语:
    • 简单 vs 简陋
    • 平衡 vs 妥协
    • 迭代 vs 半成品

推荐序二

1.古老的程序员知识并没有过时(面向对象的编程原则、编程范式),不少如今光鲜的架构还是在解决古老的问题,需要聆听久远的教诲,遵循古老的智慧。

序言

  1. 软件架构学关注的一个重点是组织结构,好的架构层次清晰,一目了然。
  2. 无论多好的方案都必须理解和遵守现实的条件约束。
  3. 需要付出的事件、金钱和人力成本是区分架构规模大小的衡量标准,也可以用来判断某个特定架构的好坏:一个好的架构,不仅要在某一特定时刻满足软件用户、开发者和所有者的需求,更要在一段时间内维持满足他们后续的需求。
  4. 在一个好的架构中,常规变更不应该是成本高昂的,也不应该是难以解决的大型设计调整,更不需要单独立项来推进。这些常规变更应该要能融入到每日或者每周的日常系统维护中去。
  5. 在遵循指导原则情况下不断调整,不断实践。

前言

  1. 从计算机出现开始到今天,软件的本质都是一样的,都是由if语句、赋值语句以及while循环组成的。不管任何编程语言,最终产生的代码任然只是顺序结构、分支结构、循环结构的组合,这与计算机刚出现时的程序是一模一样的。
  2. 成绩算计出现开始到今天,计算机的本质基本没有什么大变化。编程语言稍微进步了一点,工具质量大大提升,但是计算机程序基本构造没有什么变化。
  3. 软件架构的规则就是排列组合代码块的规则。由于代码块的本质没有变化,因此排列组合他们的规则也就不会变化。

第一部分 概述

  1. 好的软件架构可以大大节省软件项目构建与维护的人力成本,让每次变更都短小简单,易于实施,并且避免错误,用最小的成本最大程度的满足功能性和灵活性的要求。

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

  1. 设计与架构,两者的概念没有任何区别。”架构“一词往往用于”高层级“的讨论中,而这类讨论一般都把”底层“的实现细节排除在外。而”设计“一词往往指具体的系统底层组织结构和实现的细节。但是这样的区分根本不成立。底层细节设计与顶层架构信息是不可分割的。它们组合在一起,共同定义了整个软件系统,缺一不可。所谓底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线
  2. 软件架构的终极目的:用最小的人力成本来满足构建和维护该系统的需求
  3. 一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量
  4. 无论从长期还是短期来看,胡乱编写代码的工作速度其实比循规蹈矩更慢。
  5. 软件开发的核心特点:想要跑的块,先要跑的稳

第2章 两个价值维度

  1. 每个软件系统都可以通过行为架构两个维度来体现他的实际价值。
  2. 行为价值:软件表现的行为符合用户需求,不出bug。这是软件系统最直观的价值维度。
  3. 架构价值:该价值就体现在software这个英文单词上,ware是指产品,而soft就是指软件的灵活性。软件系统必须保持灵活性,软件发明的目的就是以一种灵活的方式来改变机器的行为(机器上那些难以改变的行为通常称之为硬件)。架构设计就是灵活性的保障,好的架构要能轻松的应对各种灵活的常规修改。需求变更的实施难度应该与变更的范畴成等比关系(变更范畴应该是指变更所影响的范围,变更范畴以及变更难度也都受架构的影响)。
  4. 架构价值更为重要:如果某程序能运行,但需求变更后无法通过修改让它满足需求且继续正常工作,那这个程序就没价值(这里的无法修改并不是说真的无法修改,理论上任何程序都能修改,这里说的是实施变更的成本远远超过变更带来的价值)。相反,即便某程序现在无法工作,但是如果很容易能将它修好,并且随着正常需求的变化都很容易通过修改程序而实现,那这个程序就会持续产生价值。

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

1.编程范式是指程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构。

第3章 编程范式总览

  1. 结构化编程:采用子程序、程序码区块、for 循环以及 while 循环等结构,来取代传统的 goto 。希望借此来改善计算机程序的明晰性、品质以及开发时间,并且避免写出面条式代码。结构化编程对程序控制权的直接转移进行了限制和规范
  2. 面向对象编程:本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征,只能对事物特征和变化规律的一种抽象,且在它所涉及的范围内更普遍、更集中、更深刻地描述客体的特征。通过建立模型而达到的抽象是人们对客体认识的深化。面向对象编程对程序控制权的间接转移进行了限制和规范
  3. 函数式编程:属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。函数式编程对程序中的赋值进行了限制和规范
    4.软件架构的三大关注重点:功能性组件独立性数据管理

第4章 结构化编程

  1. 数学是要将可证明的结论证明,而科学则是要将可证明的结论证伪。
  2. 测试只能展示bug的存在,而不能证明bug的不该存在。测试的作用是让给我们得出某段程序已经足够实现当前目标这一结论。
  3. 结构化编程范式中最有价值的的地方就是赋予了我们创造可证伪程序单元的能力,这也就是为什么在架构设计领域,功能性讲解拆分仍然是最佳实践之一
  4. 无论哪一个层面,从最小函数到最大组件,软件的研发过程都和科学研究非常类似,它们都是由证伪驱动的。软件结构是需要定义可以方便的地进行证伪(测试)的模块、组件以及服。为了达到这个目的,他们需要将类似的结构化编程的限制方法应用在更高的层次上

第5章 面向对象编程

  1. 面向对象编程的三大特性:封装、继承和多态其实并不是面向对象编程送独有的。C 语言相对 C++ 来说其实也已经具有这些特性(或者说 C 语言也已蕴含这三大特性的精髓)。
    • 封装:封装可以把一组相关联的数据与函数圈起来,使圈外的代码只能看见部分函数,数据则完全不可见。分离式编译的c代码其实是完美的封装,声明与实现分离,对外暴露接口,内部细节可以完美隐藏。反而是 C++ 的类要求成员变量声明在头文件中(因为 C++ 编译器必须知道类所占的内存大小)破坏了这种完美封装。
    • 继承:继承可以在某个作用域内对外部定义的一组变量与函数进行覆盖。C 语言通过含有相同内存布局部分的两个结构之间的类型强转同样可以实现。
    • 多态:C 语言通过在不同设备上相同功能模块(如不同设备上的声卡驱动)上强制规定接口名称以及参数,调用的时候使用函数指针就可以实现多态。归根结底,多态不过是函数指针的一种应用
  2. 对以一个架构师来说,面向对象编程的含义是非常明确的:面向对象编程就是以多态为手段来对源码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署

第6章 函数式编程

  1. 函数式编程中的变量是不可变的。
  2. 不可变性是软件架构设计中需要考虑的重点,因为一切的竞争、死锁、并发问题都是由可变变量导致的。
  3. 一个架构设计良好的应用程序应该将状态修改的部分和不需要修改的部分隔离成单独的组件,然后用合适的机制来保护可变量。软件架构师应该着力于将大部分处理逻辑归于不可变组件中,可变状态组件的逻辑应该越少越好

第三部分 设计原则

第7章 SRP:单一职责原则

  1. SRP:任何一个软件模块都应该只对某一类行为者负责。
  2. 单一职责原则主要讨论的是函数与类之间的关系,但它在两个讨论层面上会议不同的形式出现。在组件层面,我们可以将其称为共同闭包原则( Common Closure Principle )。在软件架构层面,它则是用于奠定架构边界的变更轴心( Axis of Change )。

第8章 OCP:开放封闭原则

  1. OCP:设计良好的计算机软件应该易于扩展,同时抗拒修改。也就是说,一个设计良好的计算机系统应该在不需要需改的前提下就可以轻易被扩展。
  2. OCP 是系统架构设计的主导原则,目标是让系统易于扩展,同时限制其每次被修改的所影响的范围。实现方式是通过将系统划分为一系列组件,并且将这些组件之间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

第9章 LSP:里氏替换原则

  1. 如果想用可替换的组件来构建软件系统,那么这些组件必须遵守同一个约定,以便这些组件可以相互替换。
  2. LSP 不仅是指导如何使用继承关系的一种方法,它也是一种更为广泛的、指导接口与其实现方式的设计原则。(这里的接口可以有多种形式,可以是 Java 风格接口,具有多个实现类;也可以像 Ruby 一样,几个类共用一样的方法签名,甚至可以是几个服务响应同一个 Rest 接口,LSP 适用这些场景,因为这些场景中的用户都依赖于一种接口,并且都期待实现该接口的类之间能具有可替换性)。

第10章 ISP:接口隔离原则

  1. LSP:在软件设计中应避免不必要的依赖。任何层次的软件设计如果依赖了它所不需要的东西,就会带来意料之外的麻烦。

第11章 DIP 依赖反转原则

  1. DIP:如果想要设计一个灵活的的系统,在源代码层次的层次的依赖关系中应该多引用抽象类,而非具体实现。
    • 应该在代码中多使用抽象类,尽量避免使用那些多变的具体实现类(通常使用抽象工厂模式)。
      *不要再具体实现类上创建衍生类(继承关系是所有一切源代码依赖关系中最强、最难以修改的,所以对继承的使用要格外小心)。
    • 不要覆盖包含具体实现的函数(尽量调用抽象函数)。
    • 避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。

第四部分 组件构建原则

第12章 组件

  1. 组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。在编译运行语言中,组件是一组二进制文件组合。在解释型语言中,组件是一组源代码文件的组合。
  2. 在程序开发早期,程序员可以完全掌控自己编写的程序所处的内存地址和存放格式。那时,程序的第一条语句被称为起源语句,它的作用的是声明该程序有应该被加载到的内存位置。这种程序基本不能被重定位。在调用函数库的时候程序员需要将所有要调到的函数库源代码包含到自己测程序中,然后再整体编译。函数库文件都是以源码而非二进制的形式保存的。但这种方式会使编译十分耗时。
  3. 为了缩短编译时间,程序员们将函数库的源代码单独编译。函数库的源代码在编译后会被加载到一个指定位置(固定地址)。然后编译器会针对该库文件创建一个符号表,将其和应用程序编译在一起。当程序运行时,先会加载二进制形式的库文件,再加载编译后的应用程序。但是当应用程序较大时,一块空闲内存放不下,那就得将程序代码分割存放在多处。但是程序和函数库的碎片化程度就会随着计算机内存的增加而不断增加。
  4. 为了解决内存碎片化问题,出现了重定位计数。其原理就是程序员修改修改编译输出文件的二进制格式,使其可以有由一个智能加载器加载到任意内存位置。在加载器启动时需要为这些文件指定要加载到的内存地址,而且可重定位的代码中还包含了一些符号,加载器将其加载到指定位置时会修改这些记号对应的地址。一般来说,这个过程不过就是将二进制文件中包含的内存地址都按照其加载到的内存基础位置进行递增。除此之外,程序员还对编译器进行了另一个修改,就是在可重定位的二进制文件中将函数名输出为元数据并存储起来。这样一来,如果一段程序调用了某个库函数,编译器就会将这个函数名称输出为外部引用( external referance ),而将库函数的定义输出为外部定义( external definition )。加载器在加载完成后,会将外部引用和外部定义链接( link )起来。这就是链接加载器( linking loader )的由来。
  5. 连接加载器让程序员们可以将程序分割成多个可被分别编译、加载的程序段。在程序规模小、链接少的情况下,这个方案一直很好用,但随着程序规模的大幅增长,链接加载器的处理速度跟不上了。后来程序员们只能将加载和链接的过程也进行分离。将耗时较长的部分——链接部分放在了一个单独的程序中去进行,这个程序就是所谓的链接器。链接器的输出是一个已经完成了外部链接的、可重定位的二进制文件,这种文件可以由一个支持重定位的加载器迅速加载到内存中。这使得程序员可以用缓慢的连接产生出可以很快进行多次加载的可执行文件。

第13章 组件聚合

  1. 与构建组件相关的基本原则: REP (复用/发布等同原则) CCP (共同闭包原则) CRP (共同复用原则)

  2. 复用/发布等同原则:软件复用的最小粒度应等同于其发布的最小粒度。从软件设计和架构设计角度来看,ERP 原则就是指组件中的类与模块必须是彼此紧密相关的。也就是说一个组件不能由一组毫无关联的类和模块组成,它们之间应该有一个共同的主题或者大方向。但 REP 原则的薄弱性在于没有清晰的定义出到底如何将类与模块组合成组件( CCP 和 CRP 会从相反的角度对这个原则进行有力的补偿)。

  3. 共同闭包原则:我们应该将那些会同时修改,并且为相同目的而修改的类放到的同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同组件中。CCP 的主要作用就是提示我们要将所有可能会(战略性选择闭包范围)被一起修改的类集中在一处。也就是说,如果两个类紧密相关,不管是在源代码层面还是抽象理念层面永远都会一起被修改,那么它们就应该被归属于同一个组件。当某一类变更出现时,我们需要尽可能做到改变更只影响到有限的相关组件。

    CCP 原则其实实际上就是 SRP 原则的组件版。在 SRP 原则的指导下,我们会把变更原因不同的函数放入不同的类中。而 CCP 原则指导我们将变更原因不同的类放入不同的组件中。两者可以用一句话来概括:将由于相同原因而修改,并需要同时修改的内容放在一起;将由于不同原因而修改,并且不同时修改的东西分开

  4. 共同复用原则:不要强迫一个组件的用户依赖他们不需要的东西。该原则建议我们将经常共同复用的类和模块放在同一个组件中,更重要的是告诉我们应该将哪些类分开,它实际在指导我们不紧密相连的类不应该放在同一个组件里。

    CRP 原则实际上是 ISP 原则的一个普适版。ISP 原则建议我们不要依赖我们不需要的函数的类,而 CRP 原则就是建议我们不要依赖我们不需要的类的组件。两者可以用一句话来概括:不要依赖不需要用到的东西

  5. 三大原则存在竞争关系,REP和CCP原则是粘合性原则,它们会让组件变得更大,而CRP原则是排除性原则,他会尽量让组件变小。软件架构师的任务就是要在这三大原则之间进行取舍

    若只关注REP和CRP,那么即使简单的变更也会影响到许多组件;而若只关注CCP和REP,就会导致过多的不必要的发布。优秀的软件架构师应该能在上述张力三角区域中定位一个最适合目前研发团队状态的位置,同时会根据时间不断调整。

    一般来说,一个软件项目的中心会从三角区域的右侧开始,先期主要牺牲的是复用性,随着项目的逐渐成熟,其他项目会逐渐开始对其产生依赖,项目重心就会逐渐向该三角区的左侧滑动。换句话说,一个项目在组件结构设计上重心是根据该项目的开发时间和成熟度不断变动的,我们对组件结构的安排主要与项目的开发进度和它的使用方式有关,而与项目本身功能的关系其实很小。

第14章 组件耦合

  1. 无依赖环原则( ADP ):组件关系依赖图中不应该出现环。组建的依赖结构图应该是一个有向无循环图。组件依赖结构图并不是描述应用程序功能的,它更像是应用程序在构建性维护性方面的一张图。组件依赖结构图不可能是自上而下被设计出来的,特必须随着软件系统的变化而变化,不可能在系统建设之初就被完美设计出来。

    组件结构图中的一个重要目标是指导如何频繁隔离变更。软件架构师必须设计出一套组件依赖关系图,一边将稳定的高价值组件与常变的组件隔离开,从而起到保护作用。另外随着应用程序的增长,创建可重用组件的需要也会逐渐重要起来。

  2. 稳定依赖原则( SDP ):依赖关系必须要指向更稳定的方向。任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也会变得非常难以修改。

    稳定:简单来说一个组件的稳定性好就是说由于它所依赖的组件的修改而导致它自身被修改的可能性小。计算所有输入和输出的依赖关系就可以计算出一个组件的稳定性:

    • Fan-in:入项依赖,组件外部类依赖组件内部类的数量。
    • Fan-out:出项依赖,组件内部类依赖组件外部类的数量。
    • I 不稳定性:I = Fan-out / (Fan-in + Fan-out),范围[0,1],0代表最稳定(最稳定意味着主动修改该组件的阻力最大),1代表最不稳定。

    当 I=1 时,表明该组件没有被其他组件所依赖,但该组件却依赖于其他组件。所以没有力量会干预他的变更,此时自身的主动变更难度最小;同时也因为它依赖于其他组件,所以必然会经常被动的出现变更。

    当 I=0 时,表明该组件被其他组件所依赖,但该组件没有依赖其他组件。所以来自依赖他的组件的干预力量(必须考虑变更当前组件导致依赖他的组件产生变更的影响,这便是干预/阻碍力量),此时自身的主动变更难度最大;同时也因为它不依赖于其他组件,所以不会有来自外部的变更理由。

    稳定依赖原则要求:让每个组件的I指标都必须大于其所依赖的组件的 I 指标。也就是说,组件依赖结构图中各组件的 I 指标必须按其依赖关系方向递减

  3. 稳定抽象原则( SAP ):一个组件的抽象化程度应该与其稳定性保持一致。稳定抽象原则为组件的稳定性和抽象化程度建立了一种关联关系。一方面该原则要求稳定的组件同时应该是抽象的,这样他的稳定性就不会影响到它的扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的不稳定性就可以通过具体代码而被轻易修改。

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

    SDP 和 SAP 结合起来就等于组件上的 DIP , SDP 要求让依赖关系指向更稳定的方向,而 SAP 则告诉我们稳定性本身就隐含了抽象化的要求,即依赖关系应该指向更抽象的方向

  4. 假设 A 指标是对组件抽象化程度的衡量,它的值是组件中抽象类和接口所占的比例。

    • Nc :组件中类的数量。
    • Na :组件中抽象类和接口的数量。
    • A :抽象程度,A =Na / Nc,取值范围[0,1]。

    不稳定性 I 与抽象化程度 A 之间的关系:A 为纵轴,I 为横轴,最稳定的、包含无限抽象类的组件应该位于左上角 (0,1) ,最不稳定的、最具体的组件应该位于右下角 (1,0) 。

    当然,不可能所有组件都位于这两个位置上,因为组件通常都有各自的稳定程度和抽象程度。例如抽象类有时会衍生出另一个抽象类,衍生过程就意味着依赖关系,因此该组件虽然是抽象的,但并不是完全稳定的,依赖关系降低了它的稳定性。

    • 若组件位于 (0,0) 位置,那么一定是非常稳定但也非常具体的组件。由于组件既不抽象又由于被很多组件所依赖而很难被修改,所以我们不希望一个设立良好的组件贴近这个区域,因此 (0,0) 周围的区域为称为痛苦区。如数据库表结构、工具库等。不仅可变组件落在痛苦区是无害的,因为它们不太可能发生变化,只有多变的组件落在痛苦区才会造成麻烦。

    • 无用区:若组件位于(1,1)位置,那么这个组件是无限抽象并且没有其他组件依赖它的,这样的组件往往无法使用,因此将因此(1,1)周围的区域为称为无用区

    • 很明显,最多变的组件应该离痛苦区和无用区越远越好。将离这两个区最远的点连成一条线,即从(0,1)到(1,0),这条线称为主序列线

      主序列线上的的组件不会为了追求稳定而被设计的太过抽象,也不会为了避免抽象化而设计的太过稳定。这样的组件既不会太难以被修改,又可以实现足够的功能。对于这些组件来说,通常会有足够多的组件依赖于它们从而使得他们会有一定的抽象程度,同时他们也依赖了足够多的其他组件,这使得它们也一定包含很多具体实现。

      在整条主序列线上,组件所能处于最优的位置是线的两端。一个优秀的软件架构师理想状况下应该争取将自己的设计的大部分组件尽可能地推向这两个位置。但实际上大型系统中的组件及不可能完全抽象也不可能完全稳定,因此实际中只要追求让这些组件位于主序列线上,或者贴近这条线即可

    • 如果让组件位于或者靠近主序列线是可取目标,那么可以创建一个指标来衡量一个组件距离最佳位置的程度。

      D指标:D = | A + I -1 |,取值范围[0,1],值越大表明离主序列线越远。

      通过计算D指标,就可以量化一个系统设计与主序列线的契合程度。也可以用D指标大于多少来指导决定组件手否需要重构与重新设计。另外还可以计算所有组件D指标的平均值和方差,用统计学的方法来量化分析一个系统设计:设计良好的系统的D指标的平均值和方差应该趋近于0。方差还可以被当作组件达标红线来使用,通过它可以找出系统中那些不合理的组件设计。

第5部分 软件架构

第15章 什么是软件架构

  1. 软件架构师必须是程序员,而且是能力最强的程序员。在不断地承接编程任务的过程中感受系统设计所带来的麻烦,从而逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。

  2. 软件架构的实质:规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间的互相通信的方式。

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

  4. 如果想设计一个便于推进各项工作的系统,其策略就是在设计中尽可能长时间的保留尽可能多的可选项。

  5. 架构设计对于软件开发各阶段的意义:

    • 开发:一个开发起来很困难的软件系统一般不太可能会有一个长久的、健康的生命周期,所以系统架构的作用就是方便其开发团队对其的开发。
    • 部署:为了让开发成为有效的工作,软件系统必须是可部署的。通常情况下,一个软件系统的部署成本越高,可用性就与越低。所以,实现一键式的轻松部署应该是设计软件架构的一个目标。
    • 运行:设计良好的系统架构应该可以使开发人员对运行的过程一目了然。架构应该起到揭示系统运行过程的作用。具体来说,就是架构应该将系统中的用例、功能以及该系统的必备行为设置为对开发者可见的一级实体,简化他们对于系统的理解,这将为整个系统的开发与维护提供很大的帮助。
    • 维护:系统维护的主要成本集中在“探秘”和“风险”这两件事情上。“探秘”的主要成本主要来自于在我们对现有软件系统的挖掘,目的是确定新增功能或被修复问题的最佳位置和最佳方式。 而“风险”则是指我们进行上述修改时,总有可能衍生出新的问题,这种可能性就是风险成本。我们可以通过精雕细琢的架构设计极大地降低这两项成本。通过将系统切分为组件,并使用稳定的接口将组件隔离,我们可以将未来新功能的添加方式明确出来,并大幅降低在修改过程中对系统其他部分造成伤害的可能性。
  6. 维持软件灵活性的的方法就是长时间的保留尽可能多的可选项。那些无关紧要的细节就是可选项。

    软件基本上都可以分解为策略和细节这两个元素(即框架和实现)。架构师的目的就是创建一种系统形态,该形态会议策略为基本元素,让策略与细节脱离关系,以允许在具体决策过程中推出或延迟与细节相关的内容(即标准化框架流程与具体业务实现的分离,通过多态、回调函数等方式实现)。

    即使已经指定了软件某功能会采用某实现,架构师应当假装不知道,应尽可能的设计出替换能力。一个优秀的软件架构师应致力于最大化可选项数量

  7. 一个优秀的架构师会小心的将软件的高层策略和其底层实现隔离开,让高层的策略与底层的细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,一个优秀的架构师设计的策略应允许系统尽可能的推迟与实现细节相关的策略,越晚做决策越好

第16章 独立性

  1. 一个设计良好的架构应该通过保留可选项的方式,让系统在任何情况下都能方便地做出必要的变更

  2. 横向解耦:按层解耦,也可以叫做按功能解耦,各功能模块之间边界清晰,通过接口协议进行调用。

  3. 纵向解耦:也叫用例解耦,即按用例场景对横向解耦的各功能进行纵向切片,切分出用例场景对个功能之间的依赖关系。这样就可以保证用例之间的独立性,增删用例不会影响任何其他用例。

  4. 横向/纵向解耦结合,进行网格分层,系统就可以支持多团队开发,不管团队的组织形式是分功能开发、分组件开发、分层开发亦或是按什么其他方式分功,都能支持。且这种解耦方式也会给系统部署带来极大的灵活性,甚至可以在系统运行过程中进行热切换(hot-swap)各分层实现和具体用例。

  5. 不要害怕重复,要区分是真重复还是假重复,比如两段代码的的演进路径不同,他们有着不同的变更速率和变更原缘由,那他们就并不是真正的重复。因为这是必须的,因为或许现在看起来代码是一样的,过一段时间在看代码可能就完全不同了。要仔细均衡考虑复用和重复的合理性

  6. 横纵向解耦有多种方式:

    • 源码层次:通过控制模块代码之间的依赖关系来实现解耦。这类系统运行之时是以一个单一的文件(exe)加载到计算机内容中的,各代码都执行在同一个地址空间中,人们常把这种模式称为单体结构
    • 部署层次:控制部署单元(如dll、jar等)之间的依赖关系来解耦。这种模式下,大部分组件还是运行在同一个地址空间,而有的组件可能运行在同一个处理器的其他进程下,通过socket、共享内存等进行跨进程通信。
    • 服务层次:将组件的依赖关系降低到数据级别,仅通过网络来通信。这样系统的每一个执行单元,在源码层和二进制层都是一个独立的个体,它们的变更不会影响到其他地方(例如常见的服务或微服务都是如此)。

    哪种模式最好?事实上,在项目早期,很难知道哪种模式好,随着项目的逐渐成熟,最好的模式可能会发生变化。一个设计良好的架构应该能允许一个系统从单结构开始,以单一文件的形式部署,然后逐渐成长为一组相互独立的可独立的可部署的单元,甚至是独立的服务或者微服务。最后还能随着情况的变化,允许系统逐渐退回到单体结构

  7. 对于一个系统来说,解耦模式也应该是个可选项。在进行大型部署时可以采用一种模式,在进行小型部署时则可以采用另一种模式。

  8. 注意:系统所适用的解耦模式可能会随着时间而变化,优秀的架构师应该能预见这一点,并做出相应的对策。

第17章 边界划分

  1. 软件架构设计本身就是一门划分边界的艺术
  2. 边界的作用是将软件分割成各种元素 ,一边约束边界两侧之间的依赖关系。
  3. 有一些边界是在项目初期,甚至是在编写代码之前就已经划分好的,而有些边界则是后来才划分的。项目初期的这些边界划分的目的是方便尽量将一些决策延后,并确保未来这些决策不会对系统的核心业务逻辑产生干扰
  4. 一个系统开发维护中最耗费人力资源的即使系统中的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合。
  5. 那些与系统的业务需求(也就是用例)无关的决策会被认为是过早或者不成熟的。包括我们要用的数据库、web服务、工具库等。一个设计良好的系统架构应该尽量推迟这些细节性的决策,并致力于将这种推迟所产生的影响降到最低
  6. 边界应该画在那些不相关的事情之间
  7. 软件架构应该向着插件式架构发展,从而构建一个可扩展、可维护的系统架构。

第18章 边界分析

  1. 除单体结构以外,大部分系统都会采用多种边界划分策略。一个按照服务层次划分边界的系统也可能会在某一部分采用本地进程的边界划分模式。
  2. 事实上,服务经常不过是一系列相互作用的本地进程的某种外在形式。无论是服务还是本地进程,他们几乎都肯定是由一个或多个源码组件组成的单体结构,或一组动态链接的可部署组件。
  3. 一个系统中通常会包含高通信量、低延迟的本地架构边界和低通信量、高延迟的服务边界。

第19章 策略与层次

  1. 本质上所有软件系统都是一组策略语句的集合。可以说计算机程序不过就是一组仔细描述如何将输入转化为输出的策略语句的集合。
  2. 软件架构设计工作的重点之一就是将策略彼此分离,然后将他们按照变更方式进行重组。其中变更原因、时间和层次相同的的策略应该被分到同一个组件中。反之,则应该分属不同的组件。
  3. 软件架构的工作常常需要将组件重排组合成一个 有向无循环图。图上的每一个节点代表一个拥有相同层次策略的组件。每一条单向链接代表了一种组件之间的依赖关系,它们将不同级别的组件链接起来。
  4. 对“层次”的定义是严格按照“输入与输出之间的距离”来划分的。一条策略离系统的输入/s输出越远,它所属的层次就越高。而直接管理输入/输出的策略在系统中的层次是最低的。
  5. 通过将策略隔离,并将源码种的依赖方向都统一调整为指向高层策略,可以大幅度降低系统变更所带来的影响。因为一些针对底层组件的紧急小修改几乎不会影响系统中更高级、更重要的组件。

第20章 业务逻辑

  1. 业务实体是计算机系统中的一组对象,包含了一系列用于操作关键数据的业务逻辑。这些业务实体对象要么直接包含关节业务数据,要么可以很容易访问这些数据。
  2. 业务实体的接口层是由那些实现关键业务逻辑、操作关键业务数据的函数组成。
  3. 业务用例是使用业务实体的具体场景。业务实体无需了解业务用例。业务用例属于低层概念,而业务用例是业务功能抽象,属于高层概念。因此业务用例是依赖业务实体的,而业务实体并不依赖于业务用例。
  4. 用例所接收的输入应是一个简单的请求性数据结构,输出的应该是一个简单的输出性数据结构。这些数据结构之间不应存在任何的依赖关系(做到各层数据隔离,这很重要)。

第21章 尖叫的软件架构

  1. 一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。 良好的架构设计应只关注用例,并能将他们与其他周边因素隔离

第22章 整洁结构

  1. 过去地几十年中出现了很多架构(六边形架构、DCI架构、BCE架构等),虽然这些架构在细节上各有不同,但总体来说是非常相似的:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。这些架构通常都具有以下特点:

    • 独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数,框架可以被当成工具来使用,但不需要让系统来适应框架。
    • 可被测试:这些系统的业务逻辑可以脱离UI、数据库、Web服务以及其他的外部元素来进行测试。
    • 独立于 UI:这些系统的 UI 变更起来很容易,不需要修改其他的系统部分。
    • 独立于数据库:可以轻易地更换所使用的数据库。
    • 独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。
  2. 下图中同心环代表软件系统中的不同层次,通常越靠近中心,其所在的软件层次就越高。基本上,外层圆代表的是机制,内层圆代表的是策略。

    这其中有一条贯穿整个架构设计的规则,即它的依赖关系规则:源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略

    换句话说,就是任何属于内层圆中的代码都不应该牵涉外层圆中的代码,尤其是内层圆中的代码不应该引用外层圆中代码所声明的名字,包括函数、类、变量以及一切其他有命名的软件实体。

    同样的道理,外层圆中使用的数据格式也不应该被内层圆中的代码所使用,尤其是当数据格式是由外层圆的框架所生成时,总之,我们不应该让外层圆中发生的任何变更影响到内层圆的代码。

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

      如果我们在写的不是一个大型系统,而是一个单一应用的话,那么我们的业务实体就是该应用的业务对象,这些对象封装了该应用中最通用、最高层的业务逻辑,它们应该属于系统中最不容易受外界影响而变动的部分,例如,一个针对页面导航方式或者安全问题的修改不应该触及这些对象,一个针对应用在运行时的行为所做的变更也不应该影响业务实体。

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

      我们既不希望在这一层所发生的变更影响业务实体,同时也不希望这一层受外部因素(譬如数据库、UI、常见框架)的影响,用例层应该与它们都保持隔离。然而,我们知道应用行为的变化会影响用例本身,因此一定会影响用例层的代码,因此如果一个用例的细节发生了变化,这一层中的某些代码自然要受到影响。

    • 软件的接口适配层中通常是一组数据转换器,它们负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及Web)最方便操作的格式。例如,这一层中应该包含整个GUI MVC框架,展示器、视图、控制器都应该属于接口适配器层,而模型部分则应该由控制器传递给用例,再由用例传回展示器和视图。

      同样的,这一层的代码也会负责将数据从业务实体与用例而言最方便操作的格式,转化为对所采用的持久性框架(譬如数据库)最方便的格式,总之,在从该层再往内的同心圆中,其代码就不应该依赖任何数据库了,譬如说,如果我们采用的是SQL数据库,那么所有的SQL语句都应该被限制在这一层的代码中——而且是仅限于那些需要操作数据库的代码。

      当然,这一层的代码也会负责将来自外部服务的数据转换成系统内用例与业务实体所需的格式。

    • 软件最外层的模型层一般由工具、数据库、Web框架等组成的,在这一层中,我们通常只需要编写一些与内层沟通的黏合性代码。

      框架与驱动程序层中包含了所有的实现细节。Web是一个实现细节,数据库也是一个实现细节,我们将这些细节放在最外层,这样它们就很难影响到其他层了。

    图中所显示的同心圆只是为了说明架构的结构,真正的架构很可能会超过四层,并没有某个规则约定一个系统的架构有且只能有四层,然而,这其中的依赖关系原则是不变的。也就是说,源码层面的依赖关系一定要指向同心圆的内侧,层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多,最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。

    图中的右下侧展示了在架构中跨边界的情况,具体来说就是控制器、展示器与下一层的用例之间的通信过程。这里需要注意的是控制流是从控制器开始,穿过用例,最后执行展示器的代码,但源码中的依赖方向却都是向内指向用例的。这种相反性可以使用依赖反转原则(DIP)来解决,利用动态多态技术,将源码中的依赖关系与控制流的方向进行反转,不管控制流原本的方向如何,都可以让它遵守架构的依赖关系规则。

  3. 一般来说,会跨越边界的数据在数据结构上都很简单,尽量采用一些基本的结构体或简单的可传输数据对象,或者直接通过函数调用的参数来传递数据。这里最重要的是这个跨边界传输的对象应该有一个独立、简单的数据结构。

第23章 展示器和谦卑对象

  1. 第22章引入了展示器的概念,展示器实际上是一种采用谦卑对象(Humble Object)模式的一种形式。

    • 谦卑对象模式的设计目的:帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。
    • 谦卑对象模式的设计思路:将这两类行为拆分成两组模块或类。其中一组模块被称为谦卑(Humble)组,包含了系统中所有难以测试的行为,而这些行为已经被简化到不能再简化了。另一组模块则包含了所有不属于谦卑对象的行为。
  2. 对于 GUI 来讲,可以分为展示器与视图(可以理解分为数据模型模块和UI展示模块)。

    视图部分属于难以测试的谦卑对象。这种对象的代码通常应该越简单越好,它只应负责将数据填充到GUI上,而不应该对数据进行任何处理。

    展示器是可测试对象。展示器的工作室负责从应用程序中接收数据,然后按视图的需要进行格式化,以便视图将其呈现在屏幕上。

    应用程序所能控制的、要在屏幕上显示的一切东西,都应该在视图模型(也就是文中所说的展示器)中以字符串、布尔值或枚举值的形式存在。然后,视图部分除了加载视图模型所需要的值,不应该再做任何其他事情,这样才能说视图是谦卑对象。

  3. 强大的可测试性是一个架构的设计是否优秀的显著衡量标准之一。谦卑对象模式将系统行为分割成可测试和不可测试两部分的过程常常就也定义了系统的架构边界。

  4. 对于用例交互器(interactor)与数据库中间的组件,通常称之为数据库网关。这些数据库网关本身是一个多态接口,包含了应用程序在数据库上所要执行的创建、读取、更新、删除等所有操作。网关的实现属于谦卑对象。但交互器并不属于谦卑对象,因为他封装的是特定场景下的业务逻辑。虽然交互器不属于谦卑对象,却是可测试的,因为数据库网关通常可以被替换成对应的测试桩和测试替身类。

  5. 在每个系统架构的边界处,都有可能发现谦卑对象模式的存在。因为跨边界的通信肯定需要用到某种简单的数据结构,而边界会自然而然地将系统分割成难以测试的部分与容易测试的部分,所以通过在系统的边界处运用谦卑对象模式,可以大幅地提高整个系统的可测试性。

第24章 不完全边界

  1. 构建完整的架构边界很耗费成本的。在这个过程中,要为系统设计双向的多态边界接口,用于输入和输出的数据结构,以及所有相关的依赖关系管理,以便将系统分割成可独立编译与部署的组件。这里会涉及大量的前期工作,以及大量的后期维护工作。很多情况下,设计架构边界的成本太高了,但为了应对将来可能的需要,通常还是希望预留一个边界。但这种预防性设计显然违背了 YAGNI 原则(“You Aren’t Going to Need It”,意即“不要预测未来的需要”)。。然而架构师的工作本身就是要做这样的预见性设计,这时候就需要引入不完全边界(partial boundary)的概念了。
  2. 构建不完全架构边界方式:
    • 方式一:在将系统分割成一系列可独立编译、可独立部署的组件之后,再把它们构建成一个组件。

      • 优点:这种方式所需的代码量和工作量与构建完整边界完全一样,但它省去了多组件管理相关工作,等于省去了版本号管理和发布管理方面的工作。
      • 缺点:各部分之间的独立性可能会在不经意间慢慢被侵蚀,过了一段时间之后,去正真做拆分时发现不知何时耦合已经出现了。
    • 方式二:单向边界。完整的架构边界至少有两方接口,分别抽象调用方和被调用方。而且维护这种双向的隔离性,通常不会是一次性的工作,需要持续地长期投入资源维护下去,这里可以使用单向边界。

      下图所示,Client 使用的是一个由 ServiceImpl 类实现的 ServiceBoundary 接口:

      • 优点:为未来构建完整的系统架构边界打下了坚实基础。为了未来将 Client 与 ServiceImpl 隔离,必要的依赖反转已经做完了。
      • 缺点:图中的虚线箭头代表了未来有可能很快就会出现的隔离问题。由于没有采用双向反向接口,这部分只能依赖开发者和架构师的自律性来保证组件持久隔离。
    • 方式三:门户模式。

      • 优点:可以省掉依赖反转工作。边界只由 Facade 类来定义,这个类中有所有服务函数的列表,它会负责将 Client 的调用传递给对 Client 不可见的服务函数。也容易结构建立反向通道。
      • 缺点:Client 会传递性地依赖于所有的 Service 类。在静态类型语言中,对 Service 类的源码所做的任何修改都会导致 Client 的重新编译。
  3. 每种实现方式都有相应的成本和收益。每种方式都有自己所适用的场景,它们可以被用来充当最终完整架构边界的临时替代品。同时,如果这些边界最终被证明是没有必要存在的,那么也可以被自然降解。
  4. 架构师的职责之一就是预判未来哪里有可能会需要设置架构边界,并决定应该以完全形式还是不完全形式来实现它们。

第25章:层次与边界

  1. 架构边界可以存在于任何地方。作为架构师,必须要小心审视究竟在什么地方才需要设计架构边界。另外,还必须弄清楚完全实现这些边界将会带来多大的成本。同时,也必须要了解如果事先忽略了这些边界,后续再添加会有多么困难。

  2. 架构师到底是否应该在某处设计边界?这个问题恐怕没有答案:

    • 一方面,就像一些很聪明的人多年来一直告诉我们的那样,不应该将未来的需求抽象化。这就是 YAGNI 原则:“You aren’t going to need it”,臆想中的需求事实上往往是不存在的。这是一句饱含智慧的建议,因为
    • 另一方面,如果我们发现自己在某个位置确实需要设置一个架构边界,却又没有事先准备的时候,再添加边界所需要的成本和风险往往是很高的。

    作为软件架构师,必须有一点未卜先知的能力。有时候要依靠猜测——当然还要用点脑子。软件架构师必须仔细权衡成本,决定哪里需要设计架构边界,以及这些地方需要的是完整的边界,还是不完全的边界,还是可以忽略的边界

    而且,这不是一次性的决定。我们不能在项目开始时就决定好哪里需要设计边界,哪里不需要。相反,架构师必须持续观察系统的演进,时刻注意哪里可能需要设计边界,然后仔细观察这些地方会由于不存在边界而出现哪些问题

    当出现问题时,我们还需要权衡一下实现这个边界的成本,并拿它与不实现这个边界的成本对比——这种对比经常需要反复地进行。我们的目标是找到设置边界的优势超过其成本的拐点,那就是实现该边界的最佳时机

第26章 Main组件

  1. 在所有的系统中,都至少要有一个组件来负责创建、协调、监督其他组件的运转。我们将其称为 Main 组件。
  2. Main 组件是系统中最细节化的部分——也就是底层的策略,它是整个系统的初始点。在整个系统中,除了操作系统不会再有其他组件依赖于它了。
  3. Main 组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。
  4. Main 组件中的依赖关系通常应该由依赖注入框架来注入。在该框架将依赖关系注入到 Main 组件之后, Main 组件就应该可以在不依赖于该框架的情况下自行分配这些依赖关系了。
  5. Main 组件是整个系统中细节信息最多的组件
  6. Main 组件也可以被视为应用程序的一个插件——这个插件负责设置起始状态、配置信息、加载外部资源,最后将控制权转交给应用程序的其他高层组件。由于Main组件能以插件形式存在于系统中,因此我们可以为一个系统设计多个Main组件,让它们各自对应于不同的配置。当我们将Main组件视为一种插件时,用架构边界将它与系统其他部分隔离开将更容易实现。

第27章 服务:宏观与微观

  1. 面向服务或微服务的架构近年来非常流行,原因如下:
    • 服务之间似乎是强隔离的,下文会说到,并不完全是这样。
    • 服务被认为是支持独立开发和部署的,下文也会说到,并不完全是这样。
  2. 服务本身并不能完全代表系统架构。不是说“只要使用了服务,就等于有了一套架构”。架构设计的任务是找到高层策略和低层细节之间的架构边界,同时保持这些边界遵守依赖关系规则。所谓服务,本身只是一种比函数调用成本稍高的,分割应用程序行为的一种形式,与系统架构无关。服务这种形式说到底不过是一种跨进程/平台边界的函数调用而已。有些服务会具有架构上的意义,而有些则没有。
  3. 针对那些所谓的服务带来的好处,下面会一个一个来批驳:
    • 解耦合的谬论:很多人认为拆分服务的一个重要好处就是服务之间的解耦。毕竟每个服务都是以一个进程来运行的,甚至可能在不同服务器上。因此服务之间不能访问彼此的变量,其次服务之间的接口一定是充分定义的。从一定程度上来说,这个是对的。
      • 但是服务之间还是可能会通过某种共享资源,导致强耦合。例如,如果给服务之间传递的数据记录中,增加了一个新字段,那么每个需要操作这个字段的服务都必须做出相应的变更,服务之间必须对这条数据的解读达成一致。因此,这些服务实际上都强耦合这条数据结构,因此它们之间是彼此耦合的。
      • 再来说说服务能很好的定义接口,它确实能很好的定义接口。但函数也能做到这一点。事实上,服务的接口和普通函数相比,并没有比后者更正式,更严谨,也没有更好,所以这点好处根本不算什么。
    • 独立开发的谬论:人们认为另一个使用服务的好处就是,服务可以由不同的团队负责部署和运维。这可以让团队采用 dev-ops 混合的形式来编写,维护以及运维各自的服务,这种开发和部署独立性被认为是可扩展的。这种观点认为大型系统可以由几十上百个上千个独立开发部署的服务组成。整个系统的研发,运维就可以用同等量级的团队来共同完成。
      • 这种理念有一些的道理,但也仅仅是一些。无数历史事实证明,大型系统一样可以采用单体模式,或者组件模式来构建,不一定非要服务化。因此服务化并不是构建大型系统的唯一选择。
      • 其次上文说到的解耦合谬论已经充分说明拆分服务并不意味着这些服务可以独立开发、部署和运维。如果这些服务之间以数据形式或行为形式相耦合,那么他们的开发,部署,运维,也必须彼此协调进行。
  4. 服务并不一定必须是小型的单体程序。服务也可以按照 SOLID 原则来设计,按照组件架构来部署。这样就可以做到在添加/删除组件时不影响服务中的其他组件。
  5. 系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在。也就是说,服务边界并不能代表架构边界,服务内部的组件边界才是
  6. 虽然服务化有助于提升系统的扩展性和研发性,但是服务并不能代表整个系统的架构设计。系统的架构是由系统内部的架构边界,以及边界的依赖关系所定义的,与系统中的各个组件调用和通信无关

第 28 章 测试边界

  1. 测试也是一种系统组件。测试组件也要遵守依赖关系原则,它始终都是依赖于被测试部分代码的,并且不会有其他组件会去依赖测试组件

  2. 测试组件应当是可以被独立部署的,它是系统中最独立的一个组件,因为系统的正常运行,并不会需要使用到测试组件,它只是为了支持开发过程而存在的。然而,测试组件仍然是系统架构中不可缺少的组件,它在许多方面都反映了系统中其他组件所应遵循的设计模型。

  3. 由于测试组件的独立性,以及往往不会部署到正式环境,开发者通常忽略测试的重要性,这是极为错误的。测试如果没有被集成到系统设计中,往往是非常脆弱的,这种脆弱性会使得系统变得死板,非常难以更改。

    这里的关键就是耦合,如果测试代码和系统强耦合,哪怕系统一点点变更也会导致许多与之相耦合的测试需要更改。修改一个通用的系统组件,可以能导致成千上百个测试出现问题,我们将这种问题称为脆弱的测试问题

    想要解决这个问题,就必须考虑到系统的可测试性。软件开发第一要义——不要依赖于多变的东西。

  4. 设计一个可测试的系统,方法之一就是创建一套专门供测试使用的API,它应该被赋予超级权限,允许测试代码绕过安全机制以及那些成本高昂的资源(如数据库),强制将系统设置为某种可测试的状态中。设置测试API就是为了将测试的代码和应用程序的代码分开。

    • 结构性耦合:测试代码所具有的耦合关系中最强大,最阴险的。假设有一组类需要测试,每个类中的函数都有一个对应的测试函数,这就是结构性耦合。如果被测试类和其中的函数发生了变更,测试类就必须做出相应的修改。

      测试专用 API 就是将应用程序和测试代码解耦,这样可以做到两者互不影响的进行重构。这种隔离是很重要的,随着时间的推移,测试代码越趋向于具体和详细,而产品代码则会越趋向于抽象和通用。结构性的耦合可能会让这种必要的演进无法进行,或至少会形成强烈的干扰。

    • 安全性:具有超级权限的测试专用 API ,是非常危险的,所以应该将测试专用 API及其实现代码放置在一个单独的、可独立部署的组件中。

第 29 章 整洁的嵌入式架构

  1. 虽然软件并不会随着时间推移而损耗,但由于硬件更新换代是是快的软件也必须做相应改动来适配硬件的变更。也就是说,本可以长期使用的嵌入式软件可能会由于其中隐含的硬件依赖关系而无法继续使用。

  2. 什么是固件?有多种答案:

    • 固件通常被存储在非可变内存设备,例如 ROM 、 EPROM 或者闪存中。
    • 固件是直接编程在硬件设备上的一组指令或者一段程序。
    • 固件是嵌入在一个硬件中的软件程序。
    • 固件是被写入到只读内存设备中的(ROM)程序或数据。

    但实际上,大家对固件的认识普遍是错误的,或者说至少是过时的。固件并不一定是指存储在 ROM 中的代码,固件并不是依据其存储的位置来定义的。固件是由其代码依赖关系,以及随着硬件的演进在变更难度上的变化来定义的。

  3. 我们应当少写固件,多写软件。如果在代码中嵌入 了SQL 或者引入对某个平台的依赖的话,其实就是在写固件代码。

  4. 为什么很多嵌入式软件最后都成为了固件呢?很可能是因为我们在做嵌入式设计时只关注代码能否顺利运行,并不太关心其结构能否撑起一个较长的有效生命周期。 Kent Beck 描述了软件构建过程中的三个阶段:

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

    大部分“野生”的嵌入式代码,都只关注“先让它工作起来”这个目标。

  5. 嵌入式系统的程序员通常需要处理很多在写非嵌入式系统时不需要关心的事情————例如,有限的地址空间、实时性限制、运行截止时间、有限的I/O能力、非常规的用户接口、感应器,以及其他与物理世界的实际链接。大部分时候,这些系统的硬件是和它的软件、固件并行开发的,工程师在为这种系统编写代码的时候,往往没有任何地方可以运行。这就是目标硬件瓶颈(target-hardware bottleneck),它是嵌入式开发所特有的一个问题,如果我们没有采用某种清晰的架构来设计嵌入式系统的代码结构,就经常会面临只能在目标系统平台上测试代码的难题。如果只能在特定的平台上测试代码,那么这一定会拖慢项目的开发进度。

  6. 整洁的嵌入式架构就是可测试的嵌入式架构。架构设计的原则可以如下所示应用在嵌入式软件和固件上,以避免陷入目标硬件瓶颈:

    分层:分层可以有很多种方式,比如三层结构:

    底层是硬件层,由于硬件随着科技发展会不断发展变化,嵌入式工程师应当避免硬件的变动导致更多的变动,因此硬件需与其他部分隔离。

    软件与固件之间的分界线往往没有硬件与固件之间那么清晰,但我们要做的就是使这条分界线更加清晰。软件和固件中间的边界,被称为硬件抽象层(HAL)。HAL 的作用是为给上层软件部分提供服务,并隐藏硬件的实现细节(‌硬件抽象层不是驱动程序。 它位于‌操作系统内核与硬件电路之间,主要目的是将硬件抽象化,为操作系统提供一个虚拟的硬件平台,从而实现硬件无关性并简化软硬件的开发和测试过程)。

    光 HAL 层隔离还不够,软件通过操作系统来运行环境服务,为了延长代码的生命周期,必须将操作系统也做一层抽象,让代码避免与操作系统的的实现产生依赖。因此三层结构应变成四层结构

    操作系统这一层级的抽象叫做操作系统抽象层(OSAL),它将软件与操作系统分隔开。OSAL 可以帮助高价值的应用程序是现在目标平台、目标操作系统之外进行测试。

  7. 除了在嵌入式系统的主要分层(指软件、操作系统、固件、硬件这四层)之中增加 HAL 和 OSAL 之外,还可以应用其他的设计原则,这些设计原则可以帮助架构师按功能模块、接口编程以及可替代性来划分系统。

    分层架构的理念是基于接口编程的理念来设计的,当模块之间能以接口形式交互时,就可以将一个服务替换成另外一个服务。

  8. 由整洁的嵌入式架构所构建的系统应该在每一个分层中都是可测试的,因为它的模块之间采用接口通信,每一个接口都为平台之外的测试提供了替换点

第六部分 实现细节

第30章 数据库只是实现细节

  1. 请注意,这里讲的是“数据库”而非“数据模型”。数据的组织结构,数据的模型,都是系统架构中的重要部分。但是数据库并不是数据模型。数据库只是一款软件,是用来存取数据的工具。从系统架构的角度来看,工具通常是无关紧要的——因为这只是一个底层的实现细节,一种达成目标的手段,一个优秀的架构师是不会让实现细节污染整个系统架构的。总而言之,数据本身很重要,但数据库系统仅仅是一个实现细节

第31章 Web 是实现细节

  1. GUI 只是一个实现细节,而 Web 则是 GUI 的一种,所以也是一个实现细节,作为一名软件架构师,需要将这类细节与核心业务逻辑隔离开来。

第32章 应用程序框架是实现细节

  1. 框架并不等同于系统架构。
  2. 框架的作者通常会希望我们的应用与其框架紧密结合,这意味着我们将与框架签订巨大而长期的契约,而他们则不需要为我们遵守任何承诺,把风险全部交由我们自己承担。
  3. 框架的风险:
    • 框架自身架构设计可能不正确,可能进场违反依赖关系原则。
    • 随着产品需求的迭代,框架可能反而会成为我们开发的限制。
    • 框架本身可能会朝着我们不需要的方向演变。
    • 在软件的开发过程中,我们可能会更换框架。
  4. 框架可以使用,但应该将框架作为架构最外圈的一个实现细节来使用,而不是让它们进入内圈,不要让框架污染核心代码,应该依据依赖关系原则,将它们当作核心代码的插件来进行管理。尽可能长时间地将框架留在架构边界之外,业务逻辑才不会被框架影响,在需要替换框架时可以轻松替代。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值