书评:清洁建筑

对于任何认真的软件开发人员来说,这本书都是必读的。 它提供了软件开发的高级概述,而其他面向代码的书籍则没有。 主要重点是面向对象的编程以及我们如何构建可维护,灵活,可扩展和正确的系统。 但是这种方法是高级的,因此没有太多的代码示例。

本书共321页34章,不是最广泛的书,而且读起来很容易,因为很多建议都是很合理的。 但是它仍然有很多深度,我认为可以多次阅读以获取所有智慧。

这本书的另一观点来自布莱恩·奥塞普克(Blaine Osepchuk)。 我对这本书的唯一批评是,有些章节更像是介绍性内容,并且没有充分涵盖该主题。

我从这里收集了我的笔记,以使所有人受益。 但是这些注释并不能使本书获得所需的信用,因此请花点时间阅读。

第1章(什么是设计和体系结构)可以在忽略所有最佳实践的情况下构建软件。 但是随着时间的流逝,它会减慢开发速度。 一种常用的解决方法是雇用更多的开发人员。 但这只会增加问题。 当体系结构变得混乱时,需要花费更多时间对此进行推理。 并且错误的风险增加了。

快速前进的唯一方法就是前进。

作为软件开发人员,我们需要非常重视质量。 因为忽略它最终会削弱我们构建的系统。

第2章(两个值的故事)所有软件都包含两个值,即行为和结构。 行为是计算机为节省利益相关者金钱或使某些事情更有效所要做的事情。 该结构是软件的结构方式,可以轻松进行更改。

通常,重点放在行为上,以结构的危险为重。 但这对软件系统的健康很危险。 正如罗伯特所说:

  • 如果您给我提供了一个可以正常运行但无法更改的程序,那么当需求发生变化时它将无法正常运行,并且我将无法使其正常运行。 因此,该程序将变得无用。
  • 如果您给我提供了一个不起作用但很容易更改的程序,那么我可以使其正常运行,并在需求变化时保持其正常工作。 因此,该程序将继续保持有用。

两种价值观都需要保持平衡,并且开发人员的专业责任是确保不以一种为己任。

第3章(Paradigm概述)结构化,面向对象和函数式编程范例以非常有意的方式从开发人员那里删除了功能。 我们应该记住,自1968年以来就没有创建过一个新的编程范例,因此不太可能发明其他任何编程范例。

第4、5、6章(结构化,面向对象,功能性语言)在这些章节中,我们将进一步探讨每种范式如何限制用户以及它的作用。

今天的软件规则与1946年Alan Turing编写第一个代码时的规则相同。

每个范例都有不同的方式将控制权强加给开发人员。 但最后,它们非常相似。

第7、8、9、10、11章(SOLID原则)这是最重要的要理解的部分之一,它将指导您编写的每一行代码。

单身责任原则:一个模块应该对一个参与者负责 ,而仅对一个参与者负责

我们希望一段代码对一个演员负责。 例如,考虑一个计算工资并报告工时的类。 工资部分由财务部门使用,而报告的小时数由人力资源使用。

如果我们假设工资取决于小时数,我们可以采用一种在工资和报告小时数之间共享的方法来计算小时数。

当财务部门想要调整小时数的计算方式时,我们将更改共享方法。 但这迫使不需要或不需要调整的人力资源发生变化。

另一个危险是,如果两个部门都希望更改同一方法,那么不太可能由两个不同的团队来分配任务。 最终将导致合并冲突,从而给所有相关人员带来风险。

有多种方法可以避免此问题。 但是它们全部将代码移到仅支持一个参与者的不同类中。

封闭式原则:应打开软件进行扩展,并关闭软件进行修改。 我们应该能够扩展软件而无需对其进行大量更改。 该原理可同时应用于类和模块级别以及体系结构级别。

当将其应用于体系结构级别时,该原理使我们可以将组件安排到一个依赖层次结构中,以保护高级组件免受低级组件的更改。 通常通过使用接口。

L iskov替换原则:原则的目的是允许我们将组件交换为另一种实现,并确保我们的软件仍能按预期运行。 例如,将基于文件的持久性系统与数据库交换,而无需更改使用持久性系统的代码中的任何其他内容。

由于客户端可能间接取决于组件的内部状态,因此很容易以微妙的方式打破该原则。

例如,假设我们有一个对汽车建模的组件,它实现了此接口。

 公共接口Car { 
bool WheelsTurning();
}

在常规汽车上,如果车轮在转动,那么汽车正在行驶,因此客户可以使用该方法作为代理来了解汽车是否在行驶。

但是,如果我们实现了带有花哨的轮辋旋转器的汽车,则在汽车不行驶时车轮可能会转动。

由于该组件不知道客户端如何依赖它,因此很容易在不知道的情况下打破该原则。

接口隔离原理:当客户端依赖一个类时,即使它不使用该类,它也间接依赖于该类中的所有方法。 取而代之的是,为类提取接口,并使客户端依赖于该接口。

为了使依赖性最小化,我们可以将接口拆分为较小的接口。 这使客户端可以更细粒度地控制其具有的依赖项。

D依赖倒置原理:此原理通常与依赖注入混淆,后者没有直接关系。

我们应该始终努力依赖抽象而不是实现。 如果客户端直接依赖于类名,它将直接创建对该客户端的依赖关系。 这将锁定实现。 相反,我们应该依赖接口,以便以后可以更改实现。

第12章(组件):从历史上看,为系统创建插件是一项相当大的努力。 但是现在,我们可以通过在文件夹中直接添加.dll,.jar文件或其他文件来轻松地支持新插件,以供系统加载。 组件是应用程序中最小的可部署部分。

第13章(组件内聚性):我们如何知道哪些类属于哪些组件? 鲍勃叔叔使用三个原则

  • REP:重用/发布等效原则
  • CCP:通用关闭原则
  • CRP:通用重用原则

REP:应该使用版本号跟踪每个组件,因为这是我们跟踪哪些组件兼容的方式。 我们根据规则“应该有意义”一个非常弱的规则来决定在组件中包括哪些类。

CCP:组件中的类应出于相同的原因并同时更改。 组件只应有一个更改理由。 此原理与SOLID中的SRP和OCP非常相似。 仅用于组件。

CRP:当一个组件使用另一个组件时,它们形成依赖关系。 即使此依赖项仅在依赖项组件内使用单个类。 但是链接仍然一样牢固。 此原理与SOLID中的ISP有关。

不要依赖不需要的东西。

这三个原理创建了张力图,因为它们不能同时全部满足。

第14章(组件耦合):

非循环依赖原则 ,组件依赖图中绝对不能存在循环。 该书在本章中介绍了它的年代,谈论了每周构建的问题,团队中的所有开发人员都在努力整合每周的所有更改以构建发行版。

我认为Git工作流程在很大程度上解决了这个问题。 如果项目很大,尝试消除依赖关系周期也是一个好主意。 每个团队都开发其组件,并将其发布到具有版本控制的共享存储中。 然后其他团队可以重用该组件,并仅在需要或需要时才与新版本集成。

组件依赖关系图中的任何周期都会产生问题,因为每个团队无法选择何时升级每个单个组件,这会带来问题。

打破循环有两个原则。

  1. 通过创建一个可以依赖的接口并将其放在子组件中,应用依赖关系反转原理。
  2. 创建两个组件都依赖的新代理依赖项。

组件图本质上是可构建性和可维护性图。

稳定依赖原则取决于稳定性的方向。 任何易挥发的成分都不应依赖于稳定的成分。 依靠挥发性成分使它们难以更改。 使用CCP时,某些组件被设计为易失的,因此我们希望它们会发生变化。

具有大量传入依赖项的组件非常稳定,因为需要大量的努力来对其进行更改。 这导致了衡量稳定度的指标,包括扇入,扇出和不稳定性。 我尚未在任何代码分析软件中看到此指标。 在绘制类/组件图时,如果将不稳定元素放在顶部,则任何向上的箭头都将违反SDP。

稳定抽象原理 ,组件应尽可能稳定。 我们软件的某些部分应该很少更改,并应放置在稳定的组件中。 但是,我们如何确保该软件仍然足够灵活以支持我们所需的更改?

OCP(开放式封闭原则)答案; 这个原理告诉我们,我们需要创建可灵活扩展但又不需要修改的类,这意味着抽象类。

SAP原理为我们提供了稳定性和抽象性之间的关系。 结合使用SAP和SDP,可以为组件提供DIP(依赖关系反转原理)。 这意味着依赖项应朝抽象方向运行。

可以使用Nc / Na / A测量来测量这种关系

  • Nc:组件中的类数
  • Na:组件中抽象类和接口的数量
  • 答:抽象。 A = Na-Nc

给出此图,可以简化此关系以定义稳定性(I)和抽象性(A)之间的关系。

例子:

  • (0,0)高度稳定和具体,可以是数据库架构。 非常具体且易变,难以更改
  • (0,0)一个具体的实用程序库。 一个示例可能是诸如String,Bool等语言的基类。
  • (1,1)通常抽象从未实现的类或接口。

我们希望大多数代码位于主序列上。

第15章(什么是建筑?):本章的第一句话引起了我的共鸣。

首先,软件架构师是程序员,并且继续是程序员! 千万不要撒谎,这表明软件架构师从代码中撤出,专注于更高层次的问题。 他们不!

体系结构是系统的形状,这种形状的目的是促进系统的开发,部署,操作和维护。

它应该使尽可能多的选项保持打开状态,并保持尽可能长的时间。

架构的目的不是使系统正常工作,许多系统“正常”运行但架构糟糕。 该体系结构的主要目的是支持系统的生命周期,使其易于更改。

架构一开始很容易发生阻碍,因为团队很小,它可以轻松地就变更进行沟通。 但是随着团队成长为多个团队,体系结构应该能够将系统划分为定义明确的组件。

软件系统具有两种类型的值:行为和结构。 结构使软件“变软”,从而为软件带来最大价值。

我们通过保持尽可能长的打开时间来保持软件的软性。 细节应该保留,因为它们无关紧要。 任何软件系统都包含策略和详细信息。 政策是业务规则,是价值所在。 剩下的就是使系统能够与人类和其他系统(例如I / O,数据库等)进行通信的细节。

好的架构会将细节与策略分开,以使策略对细节一无所知。

领域驱动设计(Domain Driven Design)一书中建议的体系结构就是一个很好的例子,该体系结构将更多实现细节放在本书的抽象思想上。

第16章(独立性):任何体系结构的优先级都是支持系统的意图,它必须支持用例。 我们应该努力让体系结构在体系结构级别发出信号,指示该体系结构支持的行为。 购物车应用程序必须看起来像购物车应用程序(又名尖叫架构)。

有些用例每秒需要100.000个客户; 其他的则需要在大型数据集中以毫秒为单位的数据查询。 它要求架构必须允许这种结构。

它可能需要许多微服务,数千个线程或一个整体系统。 优秀的架构师会保留各种选择。 如果系统取决于体系结构,那么,如果需求从例如整体式系统切换到多服务器系统,将很难升级。

通过应用诸如SRP(单一责任原则)和CCP(通用封闭原则)之类的一些通用规则来分隔因不同原因而发生变化的事物,可以使选择保持开放。 它导致分离的层,其中UI层与业务规则分开。 甚至可以将业务规则划分开来,因为某些规则非常通用(例如输入字段的验证),而另一些规则则更具体(利息计算)。 并非所有业务规则都以相同的速率和相同的原因发生变化。

每个用例也应该分开; 它们通常不会同时或出于相同的原因而改变。 例如,向系统添加订单将不会以与删除订单相同的速率更改。

我们通过使用水平层将UI与业务规则分开。 我们对使用垂直层的业务规则也做同样的事情。

第17章(边界:界限):体系结构的目标是最大程度地减少构建和维护系统所需的人力资源。 绘制边界是为了尽可能长地推迟决策,并防止决策污染核心业务逻辑。

要划定的重要界限是在重要的事物和无关紧要的事物之间。 GUI与业务规则无关,因此它们之间应该有一条界线。 数据库也与业务规则无关,因此那里也应该有一行。

这里的关键事实是,业务逻辑可以依赖于数据库的抽象,而不依赖于数据库本身,如下图所示。

业务规则不需要知道数据库是实现为关系数据库,NoSQL存储还是任何其他持久性机制。 通过让业务规则依赖于抽象来捕获它。

GUI的边界相同,因为胖客户端和Web客户端都可能存在相同的业务规则。 业务规则不能依赖于GUI实现。 GUI也是一个细节。

边界是依赖反转原理和稳定抽象原理的应用。 依赖性箭头从较低层的细节指向抽象。

第18章(边界解剖):系统被安排成组件,并由分隔它们的边界定义。

边界的目的是包含更改,一个组件中的更改现在应该要求另一组件中的更改。 我们将“防火墙”构建到源代码中。

即使已部署的系统是一个整体,它在系统内部仍然可以具有边界以辅助开发。

边界穿越可以有不同的策略,最简单的是传递数据的函数调用。 直至通过网络调用服务。 后者是一个非常严格的边界,但是我们仍然需要严格限制边界,以免污染架构。

第19章(策略和级别):所有软件系统都执行描述输入如何转换为输出的策略。 距输入和输出的距离可以称为水平。

在一个好的架构中,依赖关系的方向应该从低到高。 一起更改的策略应分组(SRP和CCP)。

第20章(业务规则):有趣的是,本章(5页)有多小,因为它包含了引号

业务规则是软件系统存在的原因。 这是家庭的珠宝。

一些业务规则称为“关键业务规则”,即使没有软件,它们也是业务的一部分。 例子是贷款利息。 不管软件是否计算利息,如果有人这样做,它仍然可以赚钱。

关键业务规则使用的数据称为“关键业务数据”。

关键业务规则和数据封装在实体中。

并非所有业务规则都可以轻松地建模为实体,而是更像用例。 这些规则与自动化系统有关,不会有类似的非自动化表亲。

用例与实体之间存在界限。 实体不应该知道用例。 这是依赖倒置原理的一个例子。

重要的收获是要非常在意业务规则,它们应具有无与伦比的质量。

第21章(尖叫的体系结构):许多系统在构建时都考虑了技术体系结构,例如MVC,其组织结构是在一个文件夹中,模型在另一个文件夹中,而控制器在第三个文件夹中。 但是这种结构并不能说明系统的功能。

相反,我们应该集中精力使体系结构达到系统的目的。

不幸的是,根本没有解释“如何”。 重点主要放在避免将系统锁定到框架中。 并确保放置了边界,以便可以对所有用例进行单元测试。 在“域驱动设计”中,可以更好地解释尖叫的体系结构。

第22章(干净的体系结构):关于如何构建软件体系结构,存在许多不同的想法。 根据作者的说法,它们都非常相似并且在细节上大多不同,它们都努力将关注点分离。

该想法在本概述“干净的体系结构”中得到了体现。

我们越深入圈子,软件就会变得越高级。 内圈是业务规则,政策。

通常,源代码依赖项必须始终仅指向内部。 最里面的圆圈包含前面提到的关键业务规则。

用例圈包含用于协调往返实体的数据流的代码。 我们应该期望这一层的更改不会影响实体。 而且,我们应该期望对任何外部系统(如数据库)的更改都不会影响用例。

仅用例中的更改应更改用例层中的代码。

下一层包含接口适配器。 它们将最适合用例的格式转换为最适合外部使用者的格式,反之亦然。

最外层包含所有框架部分,我们应该努力在这一层中仅编写胶合代码。

注意事项:

  • 当跨边界传递数据时,我们应该转换数据以适合我们正在调用的接口。 如果从最外层获取数据库行并将其向内传递,则需要对其进行转换,因为向内的层必须不了解数据库结构。
  • 如图所示右下角所示,应该应用依赖关系反转原理来反转依赖关系。

第23章(演示者和卑鄙的对象):在类中测试某些行为可能很困难。 但是通常只有部分行为难以测试。 使用设计模式Humble对象,可以将行为分解为易于测试的部分,而将难以测试的行为剥离为基本要素。

例如,GUI很难测试,因为测试无法轻易看到屏幕,但是很多逻辑并不难测试。 因此,使用卑微的对象模式,可以拆分行为,因此可以进行测试。

另一个示例是数据库网关。 这里的SQL部分是谦虚的对象。

模式通常用作架构边界,以将易于测试与难以测试分开。

第24章(部分边界):完整的体系结构边界非常昂贵。 他们需要大量的基础架构来进行数据映射,相互的多态边界接口和依赖关系管理,以隔离双方。 它涉及大量的构建和维护工作。

但是可以创建一种占位符,该占位符的构建成本不高,但仍保留了以后转换为完整边界的可能性。

该书提到了三种实现方法:

  1. 创建所有工作,但将所有内容保留在同一组件中。 它消除了使双方独立部署的负担。
  2. 一维边界,使用策略模式,客户端可以通过公共接口与服务接口。 它确实为客户端和服务实现之间的反向通道通信打开了大门。
  3. 可以添加外观模式以隐藏服务集合。 它甚至牺牲了依赖反转。

要在这些选项之间进行选择,我们需要预测未来的需求,这当然容易出错。

第25章(层和边界):很容易将许多系统中的体系结构分为三个部分,UI,业务规则和持久性。 但这通常不足以创建灵活的体系结构。

本章中的示例是一个小游戏“ Hunt the Wumpus”。

这是一个很好的初始架构。 游戏规则与其余问题无关。 所有依赖关系都指向游戏规则。 这使我们可以添加更多的持久性选项和更多的语言选项,而无需更改游戏规则。

但是,在这种体系结构中,我们希望UI的唯一变化轴是语言,但这可能是一种幼稚的方法。 如果我们通过定义更多边界来进一步扩展体系结构,我们将获得更多选择。

新的体系结构使我们可以为UI提供不同的交付机制,同时将语言分为另一个组件,这不同于交付机制。

边界无处不在,添加边界还是忽略边界就像试图预测未来。 这是在YAGNI(您不需要它)和过度设计之间的权衡。

第26章(主要组件):在所有系统中,都有一个组件可以启动程序并监视所有内容。 即使主要成分是我们经常开始的地方,它也是所有成分中最低的。

将Main视为所有脏组件中最脏的

我们应该将Main组件视为处理配置的插件组件。 这意味着我们可以针对不同的环境,语言和客户使用不同的Main插件。

第27章(服务:大小):面向服务的体系结构是构建系统的一种流行方法。 一些原因是

  • 服务似乎强烈分离
  • 服务似乎支持独立部署和开发

没有良好的架构,原因就没有意义了。

服务架构是架构吗? 取决于体系结构是由它如何将高级策略与低级细节区分开来定义的。

如果服务架构只是归结为昂贵的函数调用,那么就没有架构。

鲍勃(Oncle Bob)解释了两个谬误:

  1. 解耦谬误 :每个服务在具有明确定义的接口的单独进程中运行。 但是,如果它们受到共享数据的约束,则它们不会解耦。 如果将新字段添加到共享记录,则需要更新对该记录类型进行操作的所有服务,并就该字段的解释达成共识。 接口也是如此; 服务接口并不比函数调用更正式。
  2. 独立开发和部署的谬误:我们的想法是我们可以通过组合多个服务来构建大型系统,其中每个服务都可以独立开发和部署。 它还将允许每个服务由不同的团队开发。 谬论在某种程度上与以上所述有关。 如果服务是通过数据或行为耦合的,那么就不能独立开发它们,需要团队之间的协调。

在本章中,将通过使用滑行聚合的软件系统解释此问题的示例。 它显示了一个简单实施的示例以及如何改善这种情况。

通过应用SOLID,我们可以走得更远。 每个服务将具有一个内部组件体系结构,该体系结构必须使独立开发和部署功能成为可能。

第28章(测试边界):测试始终取决于被测试的代码(当然)。 而且从代码到测试永远都没有依赖关系。 从体系结构的角度来看,所有测试都是相同的,它们有助于开发,通常不会部署到生产中。

我们必须始终努力设计可测试性。 要克服的第一个问题是脆弱的测试。 如果测试与系统紧密耦合,则每当系统更改时,它们都必须更改。 即使是微小的变化也可能导致许多测试失败。

软件设计的第一条规则:不要依赖易变的事物

确保可以在最低级别上测试组件,因此低级别细节上的更改不会破坏测试。 解决方案是拥有一个特定的API,该API允许测试绕过安全性和昂贵的资源。 这将是UI使用的界面的超集。

经常使用的结构是为系统中的每个类都有一个测试类。 它导致系统与测试之间的深层耦合。 测试API的职责是在测试中隐藏系统的结构,以便即使重构系统也可以对其进行测试。

我希望看到一些有关此的示例,但没有提供。

第29章(干净的嵌入式体系结构):本章对硬件/固件/软件提供了不同的观点。 该软件具有较长的使用寿命。 我们知道硬件在不断发展,我们不希望在硬件更新时更改软件。 为了帮助我们与硬件交互,无论软件在哪个硬件上运行,固件都实现了具有通用接口的桥接器。

固件之所以为固件,是因为它取决于硬件,并且随着硬件的发展很难更改。 问题在于编写固件的不仅仅是嵌入式工程师。 如果我们将SQL语句埋入我们的代码中或将依赖于平台的代码分布在我们的平台上,那么根据定义,我们已经创建了固件! 因为现在我们的软件很难更改。

本章的其余大部分内容主要是关于嵌入式编程的,以及它是如何从干净的体系结构中受益匪浅的领域。

第30、31、32章(数据库,Web,框架是一个详细信息):通常,我们首先为新系统选择一个数据库。 但是我们必须记住,数据库是一个细节。 可以将其抽象出来并完全封装。 我们的业务规则无论如何都不能依赖它。 我们应该尽可能推迟使用数据库的决定,因为在某些情况下,我们最终不需要使用该数据库。 在这种情况下,我们将避免开发过程中的所有复杂性。

Web是一个UI,因此,它也只是一个细节。 回顾第25章的游戏,我们可以将UI从系统的其余部分中抽象出来。 它为我们提供了实现多个UI的灵活性,而无需更改系统的其余部分。 如果我们实现了干净的体系结构,则可以将同一系统用于Web UI,App和桌面应用程序。

框架也只是细节。 我们不应该嫁给一个框架,因为这种关系是不对称的。 在选择框架时,我们承诺要使用它,但是框架作者没有做出任何承诺。 通常,框架建议我们紧密集成。 但这把所有风险都交给了我们。 如果该项目超出了框架的范围,那么与它进行斗争将越来越耗时,并且设施可能达不到要求。 因此,建议是保持任何框架独立无间,仅与之交互并考虑其实现细节。

第33章(案例研究:视频销售):一个完整的系统示例,其中包含了本书其余部分的所有建议。 该系统实现了视频销售和观看平台。 我没有找到很好的示例,它很简短,也没有显示我认为所有好的示例如何使用本书其余部分的建议。

第34章(缺少的一章):细节在于魔鬼,如果我们不考虑实现细节,即使是计划最充分的努力也会立即被摧毁。 这是由西蒙·布朗(Simon Brown)撰写的,并为本书的其余部分提供了一些建议。 我认为这是本书中较好的章节之一。 您应该多次阅读!

讨论了组织代码的几种不同方式:

逐层打包:我们可以轻松地组织代码,从而拥有用于控制器的软件包,用于存储库的软件包等。 但这并没有大惊小怪该架构正在实现。 随着软件的增长,需要更多的软件包来允许我们独立开发和部署。

按功能打包:另一种方法是组合所有图层,然后按垂直切片进行打包。 这是比逐层打包更好的方法,因为现在每个包都应该告诉我们有关域的信息。 但是它仍然不是最佳的。

按组件进行打包:大多数OOP语言的问题在于,我们无法轻松地在其中强制执行体系结构规则。 例如,如果我们有一个带有OrdersController,OrdersService和OrdersRepository的分层体系结构。 如果我们有从OrdersController到OrdersService的依赖关系,从OrdersService到OrdersRepository的依赖关系,则它们都必须是公共的,并且没有什么阻止开发人员直接从OrdersController到OrdersRepository的依赖关系。

避免此问题的一种方法是按组件打包。 微服务体系结构的思维方式在很多方面都有所作为。 每个组件都是独立的,并向其使用者公开一个干净的界面。 在之前的示例中,将OrdersService和OrdersRepository打包为一个组件,该组件使我们可以将OrdersRepository设为私有,并强制只有OrdersService可以访问它。

如果应用程序中的所有类型都是公共的,则程序包将成为一种组织机制,几乎没有价值。

我们可以比公开所有类型做得更好。 如果可能的话,最好使用编译器来实施体系结构规则而不是自律。


最初发布在 Datadriven-investment.com上