<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>软件架构 · Tommy Cheese</title>
    <link>https://tommycheese.github.io/tags/%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84/</link>
    <description>Tommy Cheese 的个人博客，记录软件工程、人工智能与学习实践。</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 29 Jul 2024 06:13:15 +0800</lastBuildDate>
    <atom:link href="https://tommycheese.github.io/tags/%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84/index.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>再读整洁架构之道（六）边界</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%85%AD/</link>
      <pubDate>Mon, 29 Jul 2024 06:13:15 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%85%AD/</guid>
      <description>&lt;h2 id="什么是边界"&gt;什么是边界&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;软件架构设计本身就是一门划分边界的艺术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;边界的作用是将软件划分为各种元素，以便约束边界两侧的依赖关系。在项目初期划分的边界可以方便我们将一些决策尽量延后进行，确保未来这些决策不会对系统额&lt;strong&gt;核心业务逻辑&lt;/strong&gt;造成干扰。划分边界同时可以降低或消除架构中不必要耦合，系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合（如采用的框架，数据库等元素，这些细节应该被推迟）是系统最消耗人力资源的部分。&lt;/p&gt;
&lt;p&gt;那么如何划分边界呢？可以遵循如下的基本原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;边界应该划分在那些无关的事情中间：如 GUI 与业务逻辑，这意味着输入输出（GUI）对于业务逻辑来说并不重要，业务逻辑可以选择多种不同的 GUI 实现；&lt;/li&gt;
&lt;li&gt;边界线应该沿着系统的变更轴来画。也就是说，位于边界线两侧的组件应该以不同原因、不同速率变化着。指导变更轴心的原则包括模块/类级别的SRP原则和组件级别的CCP原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在了解划分边界的重要作用，并且了解如何划分边界后，可以进一步探讨该如何划分边界了，即划分边界的基本流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先将系统分割成组件，其中一部分是系统的核心业务逻辑组件，另一部分是与核心业务无关但负责提供必要功能的插件；&lt;/li&gt;
&lt;li&gt;通过对源代码的修改，让这些非核心组件依赖于系统的核心业务逻辑组件；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;划分边界是一种对依赖反转原则和稳定抽象原则的具体应用，依赖剪头应该由底层具体实现指向高层抽象的方向。&lt;/p&gt;
&lt;h2 id="边界剖析"&gt;边界剖析&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一个系统的架构是由一系列软件组件以及它们之间的边界共同定义的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;边界具有多种形式，本章以跨边界调用为例来剖析边界。&lt;/p&gt;
&lt;p&gt;在运行时，跨边界调用指的是边界线一侧的函数调用另一侧的函数，并同时传递数据的行为。构造合理的跨边界调用需要我们对源码中的依赖关系进行合理管控，若不对依赖管控，可能导致一个模块的源码发生变更时，其他模块的源码也可能会随之发生变更或重新编译，并需要重新部署，划清边界有助于减少这种情况的发生。&lt;/p&gt;
&lt;p&gt;跨边界调用包含不同的类别：源码层次、部署层次、服务层次以及物理边界。&lt;/p&gt;
&lt;h3 id="源码层次的跨边界调用"&gt;源码层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;源码层次的跨边界调用出现在单体架构中，这类架构一般都需要利用某种动态形式的多态来管理其内部的依赖关系。&lt;/p&gt;
&lt;p&gt;最简单的跨边界调用形式，是由低层客户端来调用高层服务函数，这种依赖关系在运行时和编译时会保持指向一致，都是从低层组件指向高层组件。&lt;/p&gt;
&lt;p&gt;（原示意图暂缺）&lt;/p&gt;
&lt;p&gt;但当高层组件中的客户端需要调用低层组件中的服务时，我们就需要运用动态形式的多态来反转依赖关系（图中的Service接口是某种形式的SPI）。&lt;/p&gt;
&lt;p&gt;（原示意图暂缺）&lt;/p&gt;
&lt;h3 id="部署层次的跨边界调用"&gt;部署层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;部署层次的跨边界调用将其所有可部署的单元打包成一个便于操作的文件格式，除这一点以外，这种按部署层次解耦的组件与单体结构几乎是一样的。&lt;/p&gt;
&lt;p&gt;部署层次的跨边界调用涉及到物理边界，因为不同部署的单元需要跨越物理边界来完成通信。常见的物理边界包括动态链接库、线程、本地进程等。&lt;/p&gt;
&lt;h3 id="服务层次的跨边界调用"&gt;服务层次的跨边界调用&lt;/h3&gt;
&lt;p&gt;服务是系统架构中最强的边界形式，一个服务就是一个进程。在划分架构边界时，一定要尽可能地控制通信次数。在这个层次上通信必须能够适应高延时情况。除此之外，我们可以在服务层次上使用与本地进程相同的规则。也就是让较低层次服务成为较高层次服务的“插件”。&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（五）软件架构</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%94/</link>
      <pubDate>Sun, 28 Jul 2024 19:23:23 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%94/</guid>
      <description>&lt;p&gt;是时候进入软件架构了。在之前已经介绍了模块/类、组件的相关内容，这一部分会重点介绍软件架构的基本定义。&lt;/p&gt;
&lt;h2 id="写在最前面"&gt;写在最前面&lt;/h2&gt;
&lt;p&gt;🌟软件架构师自身需要是程序员，并且必须一直坚持做一线程序员，如果不亲身承受因系统设计而带来的麻烦，就体会不到设计不佳所带来的痛苦，接着就会逐渐迷失正确的设计方向。&lt;/p&gt;
&lt;h2 id="什么是软件架构"&gt;什么是软件架构？&lt;/h2&gt;
&lt;p&gt;软件架构工作的实质就是讨论规划如何将系统切分成组件，并安排好组件之间的排列关系与通信方式，即确定边界。&lt;/p&gt;
&lt;p&gt;软件架构设计的目的一般有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为了在工作中更好的对这些组件进行研发、部署、运行以及维护；&lt;/li&gt;
&lt;li&gt;如果想设计一个便于推进各项工作的系统，其策略就是要在设计中尽可能长时间地&lt;strong&gt;保留尽可能多的可选项&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个软件系统的架构质量和该系统是否能正常工作（行为）的关系并不大，毕竟世界上有很多架构设计糟糕但是工作正常的软件系统。真正的麻烦往往会出现在这个软件系统的开发、部署以及后续的补充开发中。这也间接反映的作者的观点：软件系统的架构价值在一定程度上大于其行为价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;软件架构设计的目标如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心目标：一个良好的架构设计应该围绕着用例来展开，良好的架构设计应该只关注用例，并能将它们与其他的周边因素隔离，隔离的方式是尽可能推迟选择，保留更多的可选项；&lt;/li&gt;
&lt;li&gt;主要目标：支撑软件系统的全生命周期，设计良好的架构可以让系统便于理解、易于修改、方便维护，并且能轻松部署；&lt;/li&gt;
&lt;li&gt;终极目标：最大化程序员的生产力（解放生产力），同时最小化系统的&lt;strong&gt;总&lt;/strong&gt;运营成本；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件架构的设计重点是将&lt;strong&gt;策略&lt;/strong&gt;彼此分离，然后将它们按照变更的方式进行重新分组，也就是划分出边界，在这一部分可以依据的原则包括SRP、CCP、CSP，SDP、SAP等等。&lt;/p&gt;
&lt;h2 id="软件架构的职责"&gt;软件架构的职责&lt;/h2&gt;
&lt;p&gt;软件架构具有开发、部署、运行以及维护多个方面的职责。&lt;/p&gt;
&lt;p&gt;在开发上：软件架构要方便软件系统的开发，因此不同的团队应该使用不同的软件架构设计。这在一定程度上也体现了康威定律。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么康维定律能够体现团队的架构？看视频了解更多：&lt;a href="https://www.bilibili.com/video/BV1bb421E7i6/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=8c0f6bcaf6b2e92f574553f45e565994"&gt;康威定律：为什么你的架构会反映团队结构？_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在部署上：软件架构要实现软件的一键式轻松部署；&lt;/p&gt;
&lt;p&gt;在运行上，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正如前面提到的，架构对于软件系统的高效运行远小于其他几个方面，作者认为这主要源于增加硬件资源可以弥补架构在运行方面的考虑不足，而架构设计通常需要更为昂贵的人力资源；&lt;/li&gt;
&lt;li&gt;除了架构设计对于高效运行的作用，架构还应该能反应系统在运行时的需求，也就是说，设计良好的系统架构应该可以使开发人员对系统的运行过程一目了然。架构应该起到揭示系统运行过程的作用，架构应该将系统中的用例、功能以及该系统的必备行为设置为对开发者可见的一级实体，简化它们对于系统的理解，《尖叫的软件架构》一章就是在重点说明这个问题；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在维护上，软件系统维护的成本一般是最高的，维护的成本可以分为&lt;strong&gt;探秘&lt;/strong&gt;与&lt;strong&gt;风险&lt;/strong&gt;两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;探秘：探秘(spelunking)的成本主要来自我们对于现有软件系统的挖掘，目的是确定新增功能或被修复问题的最佳位置和最佳方式；&lt;/li&gt;
&lt;li&gt;风险：风险(risk)，则是指当我们进行上述修改时，总是有可能衍生出新的问题，这种可能性就是风险成本；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="保持可选项"&gt;保持可选项&lt;/h2&gt;
&lt;p&gt;回忆之前提到的软件架构提到的两种价值：“行为价值”和“架构价值”，提升架构价值的方式就是让软件更软，而让软件更软的方式就是让软件架构尽可能多的保持可选项。&lt;/p&gt;
&lt;p&gt;软件系统的元素分为&lt;strong&gt;策略&lt;/strong&gt;与&lt;strong&gt;细节&lt;/strong&gt;两类，策略体现的是软件中所有的业务规则与操作过程，因此它是系统的真正价值所在，细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互，但是又不会影响到策略本身的行为。如 IO 设备、数据库等等，细节选项应该是尽可能保留的。&lt;/p&gt;
&lt;p&gt;软件架构师的目标就是要创建一种系统形态，该形态会以策略为&lt;strong&gt;最基本的元素&lt;/strong&gt;，并让细节与策略脱离关系，以允许在具体决策过程中&lt;strong&gt;推迟或延迟与细节相关的内容&lt;/strong&gt;。因为越到项目的后期，我们就拥有越多的信息来做出合理的决策。一个优秀的软件架构师应该致力于最大化可选项的数量。&lt;/p&gt;
&lt;p&gt;作者使用设备无关性的例子来说明推迟细节的重要性。现代操作系统的输入输出设备种类多样，而在计算机发展的最初时期，打孔纸带是主流（几乎是唯一）的一种输入输出方式，编程人员很自然的就将读取/输出纸带的代码耦合到了系统代码当中，当磁带出现后，开发人员不得不开发新的代码来适配磁带输入输出，光盘出现后…为了应对这种重复开发，适应性差的问题，开发人员提出了设备无关行的概念，即将设备抽象成函数，OS会利用函数与输入输出设备交互，而开发人员只需要提供这些函数的具体实现。此时输入输出就成为了OS的插件，这也是开闭原则的雏形（拥抱👏新增、抗拒🥊修改！）。&lt;/p&gt;
&lt;h2 id="保持独立性"&gt;保持独立性&lt;/h2&gt;
&lt;p&gt;良好的软件架构应该有充足的独立性。&lt;/p&gt;
&lt;p&gt;解耦可以让我们保证软件架构的独立性。&lt;/p&gt;
&lt;p&gt;解耦分为水平解耦与垂直解耦。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平解耦（按层解耦）：系统可以被解耦成若干个水平分层——UI、数据库等等；&lt;/li&gt;
&lt;li&gt;用例解耦（垂直解耦）：在水平分层解耦的同时，也按用例将其切分成多个垂直分片，如：将增加订单的用例UI与删除订单的用例UI分开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们按照变更原因的不同对系统进行解耦，就可以持续地向系统内添加新的用例，而不会影响旧有的用例。&lt;/p&gt;
&lt;p&gt;同样，解耦的层次也可以是不同的。可以在源码、部署以及服务三个不同的层次来完成解耦。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码层次：控制源码中模块之间的依赖关系，系统的组件通过函数调用来进行交互。这种模式叫做单体结构；&lt;/li&gt;
&lt;li&gt;部署层次：控制部署单元（如 Jar 文件）之间的依赖关系，系统的组件可能使用跨线程（注意不是跨网络）的通信、Socket 通信或共享内存融通信；&lt;/li&gt;
&lt;li&gt;服务层次：将组件间的依赖降低到数据结构级别，然后通过网络数据包进行通信，如微服务通过 rpc、rest 交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并没有严格的标准说明那个解耦层次是更好的，因为随着项目的逐渐成熟，最好的解耦模式可能会发生变化。一个设计良好的架构应该能允许一个系统从单体结构开始，以单一文件的形式部署，然后逐渐成长为一组相互独立的可部署单元，甚至是独立的服务或者微服务。最后还能随着情况的变化，允许系统逐渐回退到单体结构。一个设计良好的架构在上述过程中还应该能保护系统的大部分源码不受变更影响。对整个系统来说，&lt;strong&gt;解耦模式也应该是一个可选项&lt;/strong&gt;。我们在进行大型部署时可以采用一种模式，而在进行小型部署时则可以采用另一种模式。&lt;/p&gt;
&lt;p&gt;在了解解耦之后，让我们看看解耦对系统独立性到底有何影响。&lt;/p&gt;
&lt;h3 id="解耦对系统运行独立性的意义"&gt;解耦对系统运行独立性的意义&lt;/h3&gt;
&lt;p&gt;如果不同面向之间的用例得到了良好的隔离，那么需要高吞吐量的用例就和需要低吞吐量的用例互相自然分开了。如果 UI 和数据库的部分能从业务逻辑分离出来，那么它们就可以运行在不同的服务器上。而且需要较大带宽的应用也可以在多个服务器上运行多个实例。&lt;/p&gt;
&lt;h3 id="解耦对系统开发独立性的意义"&gt;解耦对系统开发独立性的意义&lt;/h3&gt;
&lt;p&gt;只要系统按照其水平分层和用例进行了恰当的解耦，整个系统的架构就可以支持多团队开发，不管团队组织形式是分功能开发、分组件开发、分层开发，还是按照别的什么变量分工都可以&lt;/p&gt;
&lt;h3 id="解耦对系统部署独立性的意义"&gt;解耦对系统部署独立性的意义&lt;/h3&gt;
&lt;p&gt;如果解耦工作做得好，我们甚至可以在系统运行过程中热切换(hot-swap)其各个分层实现和具体用例。在这种情况下，我们增加新用例就只需要在系统中添加一些新的jar文件，或启动一些服务即可，其他部分将完全不受影响&lt;/p&gt;
&lt;h2 id="什么是代码重复"&gt;什么是代码重复&lt;/h2&gt;
&lt;p&gt;代码中的重复可以分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真正的重复：代码中需要被消灭的部分；&lt;/li&gt;
&lt;li&gt;虚假的重复：看起来重复的代码，实际上是不同的演进路径，也就是拥有不同的变更速率与变更边缘。CRP阐述：对于使用频次不同的代码需要拆分到不同的组件中，如果在阅读代码时没有考虑到这一点就很有可能误认为虚假的重复为重复代码，这些“重复”有时候是必须的，还记得计算工资的例子吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第五篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>领域驱动设计初识</title>
      <link>https://tommycheese.github.io/blogs/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E5%88%9D%E8%AF%86/</link>
      <pubDate>Fri, 26 Jul 2024 19:49:45 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E5%88%9D%E8%AF%86/</guid>
      <description>&lt;h1 id="领域驱动设计概述"&gt;领域驱动设计概述&lt;/h1&gt;
&lt;p&gt;领域驱动设计（DDD, Domain-Driven Design）是一种模型驱动设计的方法，它通过领域模型捕捉领域知识，使用领域模型构造&lt;strong&gt;更易维护&lt;/strong&gt;的软件.&lt;/p&gt;
&lt;p&gt;DDD的设计过程分为战略设计与战术设计，其中战略设计面向领域、子域以及限界上下文的设计，而战术设计面向实体、值对象、领域事件等设计，关系如下：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/DDD1.png" alt="img"&gt;&lt;/p&gt;
&lt;h2 id="战略设计阶段相关概念"&gt;战略设计阶段相关概念&lt;/h2&gt;
&lt;h3 id="领域"&gt;领域&lt;/h3&gt;
&lt;p&gt;领域是系统要解决问题的领域，如商品信息管理就可以是系统要解决问题的领域.&lt;/p&gt;
&lt;h3 id="子域"&gt;子域&lt;/h3&gt;
&lt;p&gt;根据使用语言的不同可以将领域划分为不同的子域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心域：决定产品核心竞争力的子域，是最为重要、业务最核心、个性的部分；&lt;/li&gt;
&lt;li&gt;通用域：被多个子域使用的通用功能子域，比如用到的通用系统，例如认证、权限等等，这类应用没有企业特点限制，不需要做太多的定制化；&lt;/li&gt;
&lt;li&gt;支撑域：不包含核心功能与通用功能的子域，具有企业特性，但不具有通用性，例如数据代码类的数据字典等系统；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="限界上下文"&gt;限界上下文&lt;/h3&gt;
&lt;p&gt;限界上下文，即限定使用不同模型来解决不同问题所产生的不同区域.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;限界上下文是一个子域或者多个子域的集合&lt;/strong&gt;，&lt;strong&gt;要保证一个限界上下文须支持一个完整的业务流程&lt;/strong&gt;，&lt;strong&gt;保证这个业务流程所涉及的领域都在一个限界上下文中&lt;/strong&gt;.限界上下文是微服务拆分的依据，即每个限界上下文对应一个微服务.&lt;/p&gt;
&lt;p&gt;那么为什么要进行限界上下文呢？这是因为&lt;strong&gt;上下文&lt;/strong&gt;的存在导致编码十分困难，举个例子说明：&lt;/p&gt;
&lt;p&gt;例如在招聘域我们可能会使用“平台”来表明来自于哪个学校、机构；在跳水域我们使用“平台”来表明选手从那个跳台跳水；而在铁路交通运输领域，我们可能会使用“平台”来描述铁路站台……在某一天的某一个机会，跳水选手、HR和乘务员聚到了一起，并且他们都不了解对方的身份，如果此时HR问跳水选手说你来自哪个平台……&lt;/p&gt;
&lt;p&gt;看到了吧，同样是使用“平台”这个词语，但三个人在&lt;strong&gt;没有约定&lt;/strong&gt;的情况下可能对于词语的理解不尽相同，解决这个问题的办法就是把跳水域、招聘域和交通域隔离，分别定义域内的术语，在一个域内讨论就可以最大可能的避免歧义，这就是约定.这也是为什么最好把业务所涉及的领域定义在一个限界上下文中：减少歧义.&lt;/p&gt;
&lt;p&gt;在软件系统的设计中同样会遇到类似的问题，如果你负责设计一个足够大的计算机软件产品，产品涉及到跳水服务、招聘服务与乘车服务，系统没有对服务做任何切分，你在跳水有关业务逻辑中使用platform数据结构描述跳台、在招聘业务中使用platform代表平台、在交通业务中使用platform代表站台，当这三个业务不可避免的交汇到一起时，灾难就发生了，面对满屏幕不同语义但几乎同名的变量，我们能怎么办？&lt;/p&gt;
&lt;p&gt;显然，限定某一个领域术语的细节，我们就可以在领域中畅通无阻的使用这个术语了，这就是限界上下文的重要作用.&lt;/p&gt;
&lt;h2 id="战术设计阶段相关概念"&gt;战术设计阶段相关概念&lt;/h2&gt;
&lt;h3 id="值对象和实体"&gt;值对象和实体&lt;/h3&gt;
&lt;p&gt;首先了解一下值对象和实体.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值对象是通过属性值来识别的对象，即如果两个值对象的内部值都是相同的，那么我们就认为这两个值对象是相同的.显然，值对象的属性不可变.&lt;/li&gt;
&lt;li&gt;实体是拥有唯一标识和状态，且具有生命周期的业务对象.实体的属性是可变的，如果两个实体的属性是完全相同的，我们也不认为他们是相同的实体，只有他们两个的标识（如ID）是相同的才会被认为是相同的实体.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例区分值对象和实体：&lt;/p&gt;
&lt;p&gt;假如现在我们有一个白色值对象Color white{R:255, G:255, B:255}和一个轮胎实体tire{Air:&lt;em&gt;,Size:&lt;/em&gt;}：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可变性：白色中的每一个属性都是不可变的，因为一旦变化此对象就不再代表”白色“；而轮胎中的气压、尺寸等值可以改变，因为了即便这些参数发生了变化它仍然是轮胎；&lt;/li&gt;
&lt;li&gt;可比较性：正是由于值对象的值不可变性赋予了其相等的规则，两个白色对象的值一定会是相同的，因此如果两个值对象的内部值都是相同的，那么我们就认为这两个值对象是相同的，实体则由于其属性的可变性而丧失了这个特点.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于实体而言，其形态一般有四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失血模型：模型仅仅包含数据的定义和getter/setter方法，业务逻辑和应用逻辑都放到服务层中.这种类在Java中叫POJO.&lt;/li&gt;
&lt;li&gt;贫血模型：贫血模型中包含了一些业务逻辑，但不包含依赖持久层的业务逻辑.这部分依赖于持久层的业务逻辑将会放到服务层中.&lt;/li&gt;
&lt;li&gt;充血模型：充血模型中包含了所有的业务逻辑，包括依赖于持久层的业务逻辑.&lt;/li&gt;
&lt;li&gt;胀血模型：胀血模型就是把和业务逻辑不相关的其他应用逻辑（如授权、事务等）全部都放到领域模型中.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;资源库Repo&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Repo是针对Entity设计的存储操作，Repo中的操作应该尽可能低级、命名要简短，并且不能定义太多.Java中的MyBatis Mapper可以看作是Repo的一种实现.&lt;/p&gt;
&lt;h3 id="聚合与聚合根"&gt;聚合与聚合根&lt;/h3&gt;
&lt;p&gt;聚合是一种更大范围的封装，把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑，只有聚合根可以对外暴露引用，聚合也是一种内聚性的表现.聚合根之间也可相互调用，聚合根抽象出来一般名字为名词.&lt;/p&gt;
&lt;p&gt;聚合根通过以下几种手段实现封装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须通过操作聚合根来实现操作整个聚合，外部操作不允许直接操作聚合中的元素.比如紫色（外观）+轮胎+钢架+…=汽车，我们开车时不能也不会去单独操作轮胎、方向盘、外观…，相反，我们通过操作汽车这个聚合根来间接操作其他实体/值对象.&lt;/li&gt;
&lt;li&gt;聚合定义了一组&lt;strong&gt;边界&lt;/strong&gt;，边界内所有的组件必须对业务逻辑有效；&lt;/li&gt;
&lt;li&gt;必须在一个原子性的事务中操作聚合，否则可能会出现异常，也就是说聚合是操作的单元，从仓库取出聚合、操作完成后放回是一个原子性的操作；如何理解原子性？加入汽车聚合根有轮子实体、钢/铝/碳架实体，你不能拆掉汽车的一个轮子检修完后不装上.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="领域事件domain-events"&gt;领域事件Domain Events&lt;/h3&gt;
&lt;p&gt;当实体的属性发生变化时就会产生领域事件，领域事件是领域专家认为重要的事情.领域事件是发生在领域中且值得注意的事件.而领域事件通常意味着领域对象状态的改变.&lt;strong&gt;领域事件在系统中起到了传递消息、触发其他动作的作用，是解耦领域模型的重要手段之一&lt;/strong&gt;.我们往往利用&lt;strong&gt;消息队列&lt;/strong&gt;来传递领域事件，这样所有订阅此消息的子域都会进行自己内部的响应操作.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;消息总线也是一种实现方法👋.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如轮胎实体气压发生变化时，会释放领域leaked漏气事件，此漏气事件传递了“轮胎漏气”这个消息，从而引发一系列其他的响应，如动能系统变化、方向盘手感变化等等，领域事件通常会使用过去式命名来说明事件已经发生，不可撤销.&lt;/p&gt;
&lt;h3 id="领域服务domain-service"&gt;领域服务Domain Service&lt;/h3&gt;
&lt;p&gt;有些领域中的动作看上去&lt;strong&gt;并不属于任何对象&lt;/strong&gt;.它们代表了领域中的一个重要的行为，不能忽略它们或者简单地把它们合并到某个实体或者值对象中.当这样的行为从领域中被识别出来时，推荐的实践方式是将它声明成一个服务，这个服务就是领域服务.&lt;/p&gt;
&lt;p&gt;领域服务不是微服务中的“服务”概念.服务和聚合根概念有些相近，他们都可以操作多个实体，但是二者理念和作用不同，&lt;strong&gt;聚合根是对实体的组合，而服务是用于同步多个实体的状态，比如将某个item实体加入到list实体&lt;/strong&gt;（比如把mail投递到inbox）.所以Servie一般抽象出来都是动词.&lt;/p&gt;
&lt;h2 id="ddd领域建模设计领域模型"&gt;DDD领域建模（设计领域模型）&lt;/h2&gt;
&lt;p&gt;DDD领域建模一般步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据需求划分出初步的子域和限界上下文，以及上下文之间的关系；&lt;/li&gt;
&lt;li&gt;进一步分析每个上下文内部，识别出哪些是实体，哪些是值对象，并确定实体需要使用哪种代码形态：失血、贫血、充血、胀血；&lt;/li&gt;
&lt;li&gt;对实体、值对象进行关联和聚合，划分出聚合的范畴和聚合根；&lt;/li&gt;
&lt;li&gt;为聚合根设计资源库repo，并思考实体或值对象的创建方式；&lt;/li&gt;
&lt;li&gt;在工程中实践领域模型，并在实践中检验模型的合理性，倒推模型中不足的地方并重构.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是DDD采用两阶段设计原则——先进行战略设计、随后进行战术设计。&lt;/p&gt;
&lt;p&gt;在实践中，建议实体采用失血模型（实体方法只包括setter/getter）或者贫血模型（包含不涉及数据库操作的简单逻辑，如属性合法性校验）；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上这是DDD建模方式之一，是自顶向下的设计方法，还存在一种方式是使用自下而上的方法，即先确定领域模型——实体、值对象等，再确定子域、限界上下文…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（本节完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>领域驱动设计</category></item>
    <item>
      <title>再读整洁架构之道（四）组件构建原则</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%9B%9B/</link>
      <pubDate>Sat, 06 Jul 2024 17:27:18 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E5%9B%9B/</guid>
      <description>&lt;h1 id="再读整洁架构之道四组件构建原则"&gt;再读整洁架构之道（四）组件构建原则&lt;/h1&gt;
&lt;p&gt;第三篇主要叙述如何设计模块和类，在本篇将会更近一步，叙述如何进行组件的设计。&lt;/p&gt;
&lt;p&gt;组件是软件的部署单元，是整个软件系统在部署过程中可以独立完成部署的最小实体，组件可以被单独开发，组件化的插件式架构已经成为我们习以为常的软件构建形式了。&lt;/p&gt;
&lt;h2 id="组件聚合"&gt;组件聚合&lt;/h2&gt;
&lt;p&gt;组件聚合告诉我们哪些模块和类应该组合在一起形成组件。主要有三个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用/发布等同原则；&lt;/li&gt;
&lt;li&gt;共同闭包原则；&lt;/li&gt;
&lt;li&gt;共同复用原则；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="复用发布等同原则rep"&gt;复用/发布等同原则REP&lt;/h3&gt;
&lt;p&gt;REP指出软件复用的最小粒度应等同于其发布的最小粒度。&lt;/p&gt;
&lt;p&gt;REP是从代码复用角度考虑的，它规定了具备相同主题和功能的代码可以进行组合形成组件。REP建议软件复用应该按照组件来进行的。&lt;/p&gt;
&lt;p&gt;通俗来讲，某个组件打包发布后可能会形成版本号或唯一标识，我们通过引入现成代码库的方式复用代码，引入的单位就是组件打包的结果，引入的凭据就是版本号。&lt;/p&gt;
&lt;h3 id="共同闭包ccp"&gt;共同闭包CCP&lt;/h3&gt;
&lt;p&gt;CCP指出我们应该将那些会同时修改，并且为相同目的而修改的类放到同一个组件中，而将不会同时修改，并且不会为了相同目的而修改的那些类放到不同的组件中。&lt;/p&gt;
&lt;p&gt;CCP是从代码维护角度考虑的，对于由于同一原因/修改目的代码应该组合形成组件，这样就可以有效地降低因软件发布、验证及部署所带来的工作压力。&lt;/p&gt;
&lt;p&gt;前面说过，CCP是SRP的组件版，SRP和CCP都可以用以下的语言概括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将由于相同原因而修改，并且需要同时修改的“东西”放在一起。将由于不同原因而修改，并且不同时修改的东西分开。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在SRP中，东西是指函数和其他组成类或模块的成分；&lt;/li&gt;
&lt;li&gt;在CCP中，东西是指构成组件的类和模块。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="共同复用crp"&gt;共同复用CRP&lt;/h3&gt;
&lt;p&gt;CRP指出不要强迫一个组件的用户依赖他们不需要的东西。&lt;/p&gt;
&lt;p&gt;CRP规定了对于使用场景不同、使用频次不同的代码需要进行拆分到不同的组件中。CRP 是为了避免不必要的切分，是ISP接口隔离原则的普适版。&lt;/p&gt;
&lt;p&gt;CRP是接口隔离的普适版，ISP和CRP都可以用以下的语言概括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要依赖不需要用到的“东西”。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在ISP中，东西是指包含不需要的方法的函数/类；&lt;/li&gt;
&lt;li&gt;在CCP中，东西是指包含不需要的函数的类/模块；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="思考"&gt;思考&lt;/h3&gt;
&lt;p&gt;那么REP、CCP、CRP三者之间有什么关系呢？&lt;/p&gt;
&lt;p&gt;实际上三者存在着&lt;strong&gt;竞争关系&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;REP和CCP是黏合性原则，它们指导我们应该把哪些类/模块放在一起形成组件，而CRP属于排他性原则，它知道我们哪些类/模块不应该放在一起。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/image-20240706143525187.png" alt="image-20240706143525187"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果架构师只关注REP和CCP，那么会发现软件依赖了的组件中包含了很多并不需要的部分，这样会导致太多不必要的发布；&lt;/li&gt;
&lt;li&gt;如果架构师只关注CRP和CCP，那么会发现软件的复用会变得非常困难；&lt;/li&gt;
&lt;li&gt;如果架构师只关注REP和CRP，那么会发现如果部分类/模块需要变更，很多相关模块也不可避免的要跟随变更；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件架构师的任务就是要在这三个原则中间进行取舍，组件的构成安排应随着项目重心的不同，以及研发性与复用性的不同而不断演化，因此确定项目朝着哪个方向演进是要动态判断的。&lt;/p&gt;
&lt;h2 id="组件耦合"&gt;组件耦合&lt;/h2&gt;
&lt;p&gt;组件耦合告诉我们如何安排组件之间的关系。它包含三个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无依赖环原则ADP；&lt;/li&gt;
&lt;li&gt;稳定依赖原则SDP；&lt;/li&gt;
&lt;li&gt;稳定抽象原则SAP；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="无依赖环原则adp"&gt;无依赖环原则ADP&lt;/h3&gt;
&lt;p&gt;ADP告诉我们组件依赖关系图中不应该出现环。&lt;/p&gt;
&lt;p&gt;版本号控制机制解决了一觉醒来综合征的问题，而此机制需要遵循ADP，首先让我们读一下作者是怎么介绍一觉醒来综合征的：&lt;/p&gt;
&lt;p&gt;当你花了一整天的时间，好不容易搞定了一段代码，第二天上班时却发现这段代码莫名其妙地又不能工作了。这大概率是其他工作人员修改了你项目所依赖的组件。&lt;/p&gt;
&lt;p&gt;要想解决这个问题一般有两种方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每周构建&lt;/li&gt;
&lt;li&gt;版本号控制机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;u&gt;每周构建&lt;/u&gt;&lt;/strong&gt;：每个人先在自己的代码仓工作，每周固定时间（比如周五）再进行项目构建并处理可能的冲突问题。&lt;/p&gt;
&lt;p&gt;这种方式的局限性很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着项目越来越大，集成工作会越来越难以按时完成&lt;/li&gt;
&lt;li&gt;整个项目会变得越来越难以构建与测试，团队反馈周期会越来越长，研发质量自然也会越来越差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此版本号控制机制需要被引入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;u&gt;版本号控制机制&lt;/u&gt;&lt;/strong&gt;：每当一个组件发布新版本时，其他依赖这个组件的团队都可以自主决定是否立即采用新版本。&lt;/p&gt;
&lt;p&gt;版本号控制机制&lt;strong&gt;不允许组件结构依赖关系图中出现环&lt;/strong&gt;，也就是要遵守无依赖换原则ADP，否则一觉醒来综合征是不可避免的。为什么呢？&lt;/p&gt;
&lt;p&gt;这是因为如果存在依赖环路，环路里的所有组件被组合成了一个更大的组件，这就要求他们都必须使用相同的版本，这样其他的组件才能成功的完成依赖。同样，存在依赖换的系统在测试时也会成为一个棘手的问题，虽然打桩mock技术已经被广泛应用，但同时为环路中的组件重复打桩也已经十分的不优雅了。&lt;/p&gt;
&lt;p&gt;那么如何消除循环依赖呢？方案有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用依赖反转创建接口；&lt;/li&gt;
&lt;li&gt;创建新组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用DIP解决循环依赖很好理解，下面讲述一个在工作中遇到的案例来说明如何使用创建新组件来打破循环依赖。&lt;/p&gt;
&lt;p&gt;现在有这样的一个场景，团队正在使用领域驱动设计的方法设计系统架构，现在实体Entity Dog需要依赖Dog Repo完成Save存储操作。此时Entity模块依赖了Repo模块，很不幸的是在Entity模块中我们还定义了Dog PO（简直糟透了），Repo的Save又需要使用PO来完成对象的转换并存储。此时Entity和Repo之间就发生了循环依赖，幸运的是，在Go中模块之间的相互依赖是不允许的，检查起会提醒“循环导入”错误，怎么处理这个问题呢？&lt;/p&gt;
&lt;p&gt;答案是我们需要新创建一个PO模块，并把Entity中的PO移动到模块，Repo中依赖PO的函数也要划分到PO中，这样就消除了依赖环。使用依赖注入方法也是一个选择，如果你使用SprintBoot，@AutoWired即可解决此问题，但其本质也是创建了一个新组件——依赖池，消除了依赖环。&lt;/p&gt;
&lt;p&gt;类似的例子还有很多很多，是否还记得我们使用anaconda下载Python库时遇到的库版本冲突问题……&lt;/p&gt;
&lt;p&gt;在这里多插一句，笔者在设计组件结构的时候很自然的就想到要和系统功能对应，也就是说，组件应该是和系统功能一一对应的，这样组件依赖图就和系统功能模块划分也一一对应起来。想当然认为在系统设计的一开始组件依赖结构图也就产出了。&lt;/p&gt;
&lt;p&gt;但作者特别提到，自顶向下的设计是不可能的。让我们看看书中是怎么描述的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;
&lt;p&gt;组件结构图必须随着软件系统的变化而变化和扩张，而不可能在系统构建的最初就被完美设计出。因为组件依赖结构图并不和功能一一对应，它更像是应用程序在构建性与维护性方面的一张地图。&lt;/p&gt;
&lt;p&gt;被设计并实现出来的模块越来越多，项目中就逐渐出现了要对组件依赖关系进行管理的需求，我们希望将项目变更所影响的范围被限制得越小越好，因此需要应用单一职责原则(SRP)和共同闭包原则(CCP)来将经常同时被变更的类聚合在一起。&lt;/p&gt;
&lt;p&gt;组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件。&lt;/p&gt;
&lt;p&gt;另外，随着应用程序的增长，创建可重用组件的需要也会逐渐重要起来。这时CRP又会开始影响组件的组成。最后当循环依赖出现时，随着无循环依赖原则(ADP)的应用，组件依赖关系会产生相应的抖动和扩张。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="稳定依赖原则sdp"&gt;稳定依赖原则SDP&lt;/h3&gt;
&lt;p&gt;SDP告诉我们依赖关系必须要指向更稳定的方向。通常来说，要做到软件架构的越底层越稳定。&lt;/p&gt;
&lt;p&gt;任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖，否则这个多变的组件也将会变得非常难以被修改。这就是软件开发的困难之处，我们精心设计的一个容易被修改的组件很可能会由于别人的一条简单依赖而变得非常难以被修改，通过遵循SDP，这样的问题就能够被避免。&lt;/p&gt;
&lt;p&gt;判断组件是否稳定，可以使用稳定性指标。作者定义稳定性指标可以通过如下公式计算：
$$
I=\frac{Fan-out}{Fan-in+Fan-out}
$$
其中Fan-in为组件的入依赖数量，Fan-out为出依赖数量，这意味着一个组件的出依赖越少越稳定，因为如果出依赖为 0 意味着没有因素可以让组件改变。&lt;/p&gt;
&lt;p&gt;稳定依赖原则(SDP)的要求就是要让每个组件的I指标都必须大于其所依赖组件的I指标（即被指向的组件更稳定）。&lt;/p&gt;
&lt;p&gt;SDP并不是要求所有组件都应该是稳定的，我们设计组件架构图的目的就是要决定应该让哪些组件稳定，让哪些组件不稳定，所有组件都稳定的架构是不灵活的，不灵活的架构没有足够的架构价值。&lt;/p&gt;
&lt;h3 id="稳定抽象原则sap"&gt;稳定抽象原则SAP&lt;/h3&gt;
&lt;p&gt;SAP指出一个组件的抽象化程度应该与其稳定性保持一致。&lt;/p&gt;
&lt;p&gt;高层策略等应该存在稳定的组件中，但这样做会导致高层策略很难修改，幸运的是开闭原则 OCP 告诉我们稳定的组件可以被设计成容易扩展的，抽象类具有稳定抽象能力，可以设计一个抽象类具有较好的稳定性并且易于扩展和修改。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象类是接口和类之间的缓冲地带。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。原则要求稳定的组件同时应该是抽象的，这样它的稳定性就不会影响到扩展性。同时一个不稳定的组件应该包含具体的实现代码，这样它的不稳定性就可以通过具体的代码被轻易修改。因此，如果一个组件想要成为稳定组件，那么它就应该由接口和抽象类组成，以便将来做扩展。&lt;/p&gt;
&lt;p&gt;和稳定性类似，我们也可以使用抽象程度指标来衡量组件的抽象程度：
$$
A=\frac{N_c}{N_a}
$$
其中Nc是指组件中类的数量，而Na是组件中抽象类和接口的数量。A指标的取值范围是从0到1，值为0代表组件中没有任何抽象类，值为1就意味着组件中只有抽象类。&lt;/p&gt;
&lt;h3 id="思考-1"&gt;思考🤔&lt;/h3&gt;
&lt;p&gt;SDP和SAP有什么关联吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一点，从SDP、SAP与DIP上说&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上，SDP+SAP=组件DIP。因为SDP要求的是让依赖关系指向更稳定的方向，而SAP则告诉我们稳定性本身就隐含了对抽象化的要求，即依赖关系应该指向更抽象的方向。&lt;/p&gt;
&lt;p&gt;怎么理解这句话呢？&lt;/p&gt;
&lt;p&gt;可以想到，DIP在类上的作用是为了保证灵活性，SDP+SAP实际上就是为了保证组件在稳定的基础上足够灵活，并且 DIP 和 SDP+SAP 在软件架构上的影响都是具体实现类指向抽象类/接口，因此从作用上看SDP+SAP=组件DIP。&lt;/p&gt;
&lt;p&gt;但是DIP和SDP+SAP还是有不同之处，对类来说，设计是没有灰色地带的，一个类要么是抽象类，要么就不是。SDP与SAP这对原则是应用在组件层面上的，我们要允许一个组件部分抽象，部分稳定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其次，从SAP和SDP的目的达成来说&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此外，SDP指出依赖应该指向稳定的位置，SAP要求我们在设计组件时，高层的策略应该设计在抽象的组件，以便我们在架构设计时将策略与细节分开，同时便于我们进行插件式开发，SDP和SAP他们在目的上是一致的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看到了吧，这里又提到了&lt;strong&gt;插件式开发&lt;/strong&gt;这个词，上次是在哪里提到的？对，依赖反转DIP，这也说明了SAP+SDP确实类似于组件DIP。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="主序列"&gt;主序列&lt;/h3&gt;
&lt;p&gt;把组件稳定性指标I与组件抽象程度指标A结合在一起，画出一张图，a=-i+1这条直线就叫做主序列线。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tommycheese.github.io/blogimages/image-20240706171725797.png" alt="image-20240706171725797"&gt;&lt;/p&gt;
&lt;p&gt;整个区域可以分为三个区域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;痛苦区：靠近(0,0)的区域，在这个区域内的组件十分稳定但同时又十分的具体，因此不易改变，如数据库表；工具库也是处于痛苦区的组件，虽然其 I 指标为 1（工具库依赖非常多的组件，因此其不稳定），但不能被修改，否则很多代码会出现问题；&lt;/li&gt;
&lt;li&gt;无用区：处于(1,1)附近的组件，该位置上的组件通常是无限抽象的，但是没有被其他组件依赖，这样的组件往往无法使用，对于这个区域中的软件组件来说，其源码或者类中的设计问题通常是由于历史原因造成的，比如忘记清除的旧代码；&lt;/li&gt;
&lt;li&gt;主序列线区：在整条主序列线上的区域，组件所能处于最优的位置是线的两端(0, 1)与(1, 0)。一个优秀的软件架构师应该争取将自己设计的大部分组件尽可能地推向这两个位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎么衡量组件的综合性能呢？答案是计算它与主序列线的距离，来量化一个系统设计与主序列的契合程度，这个值为D值，值为0意味着组件是直接位于主序列线上的，值为1则意味着组件在距离主序列最远的位置。
$$
D=|A+I-1|
$$
这样我们就可以用D指标大于0多少来指导组件的重构与重新设计。&lt;/p&gt;
&lt;p&gt;除此以外，D还可以有更多的作用，比如计算设计中所有组件的D指标的平均值和方差，用统计学的方法来量化分析一个系统设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个良好的系统设计来说，D指标的平均值和方差都应该接近于0；&lt;/li&gt;
&lt;li&gt;方差作为组件的“达标红线”来使用，我们可以通过它找出系统设计中那些不合常规的组件；&lt;/li&gt;
&lt;li&gt;还可以按照时间追踪 D 的方差值，来观察随着时间的变化，软件系统架构的稳定和抽象变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第四篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（三）SOLID设计原则</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%89/</link>
      <pubDate>Fri, 05 Jul 2024 14:11:30 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%89/</guid>
      <description>&lt;h1 id="再读整洁架构之道三solid原则"&gt;再读整洁架构之道（三）SOLID原则&lt;/h1&gt;
&lt;p&gt;构建软件模块的主要目标有三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使软件可容忍被改动&lt;/li&gt;
&lt;li&gt;使软件更容易被理解&lt;/li&gt;
&lt;li&gt;构建可在多个软件系统中复用的组件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类，以及如何将这些类链接起来成为程序。SOLID原则指导我们如何设计模块，在架构设计层面，我们会有其他的设计原则。&lt;/p&gt;
&lt;p&gt;SOLID原则是指单一职责原则&lt;strong&gt;S&lt;/strong&gt;RP、开闭原则&lt;strong&gt;O&lt;/strong&gt;CP、里式替换原则&lt;strong&gt;L&lt;/strong&gt;SP、接口隔离原则&lt;strong&gt;I&lt;/strong&gt;SP和接口反转&lt;strong&gt;D&lt;/strong&gt;IP。&lt;/p&gt;
&lt;h2 id="单一职责原则srp"&gt;单一职责原则SRP&lt;/h2&gt;
&lt;p&gt;SRP定义每个软件模块只对一个功能负责，&lt;strong&gt;只有一个理由可以让模块改变&lt;/strong&gt;，任何一个软件模块都应该只对某一类行为者负责。&lt;/p&gt;
&lt;p&gt;组件层面的SRP被称作共同闭包原则CCP。&lt;/p&gt;
&lt;p&gt;倘若设计模块时没有遵循SRP会发生什么呢？一起看一个例子。&lt;/p&gt;
&lt;p&gt;如果财务部门、人事部门、研发部门共同依赖于一个工资、工时计算的程序，程序包含三个函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CalculateSalary：计算工资；&lt;/li&gt;
&lt;li&gt;CalculateTime：计算工时；&lt;/li&gt;
&lt;li&gt;Save：保存信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个部门平静地使用着程序，突然有一天，财务部的薪资计算发生变化，因此财务部门的维护人员更改了CalculateSalary函数；然后，诡异的事情发生了，人事部门、研发部薪资也同时变化了…&lt;/p&gt;
&lt;h2 id="开闭原则ocp"&gt;开闭原则OCP&lt;/h2&gt;
&lt;p&gt;“拥抱新增、抗拒修改！”&lt;/p&gt;
&lt;p&gt;OCP指出：如果软件系统想要更容易被改变，那么其设计就必须允许新增代码来修改系统行为，而非只能靠修改原来的代码。设计良好的计算机软件应该易于扩展，同时抗拒修改。&lt;/p&gt;
&lt;p&gt;OCP不仅适用于类和模块的设计，也适用于组件的设计。他起源于设备无关性的概念。具体来说，我们使用依赖反转将IO设备设计成插件，这样的我们就能很轻易的新增IO设备，而不去修改原有的IO设备。&lt;/p&gt;
&lt;p&gt;OCP的实现方式是通过将系统划分为一系列组件，并且将这些组件间的依赖关系按层次结构进行组织，使得高阶组件不会因低阶组件被修改而受到影响。&lt;/p&gt;
&lt;h2 id="里式替换lsp原则"&gt;里式替换LSP原则&lt;/h2&gt;
&lt;p&gt;LSP指出：如果想用可替换的组件来构建软件系统，那么这些组件就必须遵守同一个约定，以便让这些组件可以相互替换。&lt;/p&gt;
&lt;p&gt;LSP的本质是一个可替换性原则：如果对于每个类型是S的对象o1都存在一个类型为T的对象o2，能使操作T类型的程序P在用o1替换o2时行为保持不变，我们就可以将S称为T的子类型。&lt;/p&gt;
&lt;p&gt;LSP可以且应该被应用于软件架构层面，因为一旦违背了可替换性，该系统架构就不得不为此增添大量复杂的应对机制。&lt;/p&gt;
&lt;h2 id="接口隔离原则isp"&gt;接口隔离原则ISP&lt;/h2&gt;
&lt;p&gt;ISP指出：应该在设计中避免不必要的依赖，任何层次的软件设计如果依赖了它并不需要的东西，就会带来意料之外的麻烦。&lt;/p&gt;
&lt;h2 id="依赖反转dip"&gt;依赖反转DIP&lt;/h2&gt;
&lt;p&gt;DIP指出该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码，恰恰相反，那些实现底层细节的代码应该依赖高层策略性的代码。&lt;/p&gt;
&lt;p&gt;通俗理解依赖反转：这需要使用控制流和源代码依赖流来说明。控制流譬如A组件流向B组件，此时B需要知道A的实现，A也需要引入B的模块，代码依赖上也就不可避免的A组件流向了B组件。此时系统行为决定了控制流，而控制流则决定了源代码依赖关系，软件架构就别无选择。但是通过引入接口，B组件此时只需要实现接口，接口引入B，A引入接口，此时A就无需引入B模块，此时的控制流和源代码依赖是反向的，也就是“依赖反转”。&lt;/p&gt;
&lt;p&gt;依赖反转给了软件架构设计的自由，如果想要设计一个灵活的系统，在源代码层次的依赖关系中就应该多引用抽象类型（接口、抽象类等），而非具体实现。&lt;/p&gt;
&lt;p&gt;DIP可以归纳出几条编码守则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应在代码中多使用抽象接口，尽量避免使用那些多变的具体实现类；&lt;/li&gt;
&lt;li&gt;不要在具体实现类上创建衍生类；&lt;/li&gt;
&lt;li&gt;不要覆盖(override)包含具体实现的函数；&lt;/li&gt;
&lt;li&gt;应避免在代码中写入与任何具体实现相关的名字，或者是其他容易变动的事物的名字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（第三篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（二）编程范式</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%8C/</link>
      <pubDate>Fri, 05 Jul 2024 07:25:33 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%BA%8C/</guid>
      <description>&lt;h1 id="再读整洁架构之道二"&gt;再读整洁架构之道（二）&lt;/h1&gt;
&lt;p&gt;编程范式指的是程序的编写模式，它告诉我们应该在什么时候采用什么样的代码结构。&lt;/p&gt;
&lt;p&gt;截止目前，一共出现了三种编程范式：结构化编程范式、面向对象编程范式和函数式编程。&lt;/p&gt;
&lt;p&gt;作者认为，每种编程方式不是在给架构设计者的武器库进行扩充，相反，架构师和程序员的武器已经够多了，这三种编程范式是在对他们利用的武器进行&lt;strong&gt;限制&lt;/strong&gt;，这也是为什么他们叫做“范式”。&lt;/p&gt;
&lt;h2 id="结构化编程范式"&gt;结构化编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;结构化编程对程序控制权的直接转移进行了限制和规范，特别指的是限制了程序中goto的随意使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="分解程序"&gt;分解程序&lt;/h3&gt;
&lt;p&gt;Dijkstra希望使用数学推导方法对程序进行推理证明：让程序就成为了一种欧几里得结构，这样可以用一些已经证明的结构串联起新的程序，从而进一步推导整个程序的正确性。&lt;/p&gt;
&lt;p&gt;他还发现，如果程序中包含大量的goto，那么程序会变得难以分解，而goto语句的作用完全可以依靠分支和循环来完成。&lt;/p&gt;
&lt;p&gt;这样，结构化编程范式表明，程序可以进行降解拆分，一个大型问题拆分为一系列高级函数的组合，而这些高级函数各自又可以继续被拆分为一系列低级函数，如此无限递归。更重要的是，每个被拆分出来的函数也都可以用结构化编程范式来书写。&lt;/p&gt;
&lt;p&gt;但是，这种形式化证明的编程方式没有成为主流，科学证明法是目前使用较多的方法。&lt;/p&gt;
&lt;h3 id="科学证明法"&gt;科学证明法&lt;/h3&gt;
&lt;p&gt;科学理论和科学定律可以被证伪，但是没有办法被证明，类似的，程序只能测试证伪，不能证明，即“测试只能展示Bug的存在，并不能证明不存在Bug”。因此，利用科学证明法，结构化编程范式就可以促使我们先将一段程序递归降解为一系列可证明的小函数，然后再编写相关的&lt;strong&gt;测试&lt;/strong&gt;来试图证明这些函数是错误的。如果这些测试无法证伪这些函数，那么我们就可以认为这些函数是足够正确的，进而推导整个程序是正确的。&lt;/p&gt;
&lt;h2 id="面向对象编程范式"&gt;面向对象编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面向对象编程对程序控制权的间接转移进行了限制和规范。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，面向对象编程凡事使用&lt;strong&gt;多态特性&lt;/strong&gt;限制了函数指针的使用。这可以理解为函数指针只能进行有限制的指向，比如java中的多态一般出现在有继承关系的两个类的对象之间。&lt;/p&gt;
&lt;p&gt;作者也认为多态是OOP的最大特性，利用多态可以做到&lt;strong&gt;依赖反转&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;依赖反转即引入接口，完全控制系统中所有源代码的依赖关系，而不需要收到系统控制流的制约。&lt;/p&gt;
&lt;p&gt;依赖反转保证了我们可以对系统进行插件式的开发，如将web UI和数据库与业务逻辑之间的依赖进行反转，可以保证主业务逻辑与UI和数据库的解耦，这样UI和数据库就成了主业务逻辑的插件。&lt;/p&gt;
&lt;p&gt;面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力，这种能力让软件架构师可以构建出某种插件式架构，让高层策略性组件与底层实现性组件相分离，底层组件可以被编译成插件，实现独立于高层组件的开发和部署。&lt;/p&gt;
&lt;h2 id="函数式编程范式"&gt;函数式编程范式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;函数式编程对程序中的赋值进行了限制和规范。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，函数式编程语言中变量是不可变的。&lt;/p&gt;
&lt;p&gt;作者有以下的观点，这一段话来自原文：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改，那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的，那就永远不会产生死锁问题"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不考虑储存器与处理器的速度限制，那么不可变性是可行的，但实际上我们必须考虑这些因素，因此可变性只能在一定程度上可行，需要让不可变性在一定程度上可行需要完成&lt;strong&gt;可变性的隔离&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="可变性的隔离"&gt;可变性的隔离&lt;/h3&gt;
&lt;p&gt;可变性隔离的一种常见方式是将应用程序，或者是应用程序的内部服务进行切分，&lt;strong&gt;划分为可变的和不可变的两种组件&lt;/strong&gt;。不可变组件用纯函数的方式来执行任务，期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件（即可变组件）通信的方式来修改变量状态。&lt;/p&gt;
&lt;p&gt;一个例子是GIT版本管理工具。它通过移动指针来记录文件的变化：增删改，但实际上文件并没有被真正的修改和删除，只存在增加和检索查询两种情况，这不就是不可变性的一个例子吗。&lt;/p&gt;
&lt;p&gt;同时，mysql中的事务管理、事务性内存的工作也都运用了这个思想。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;三种编程范式与软件架构具有密切的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多态是我们跨越边界的手段；&lt;/li&gt;
&lt;li&gt;函数式编程是我们规范和限制数据存放位置与访问权限的手段；&lt;/li&gt;
&lt;li&gt;结构化编程则是个模块的实现基础；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三种编程范式与软件架构的三大关注重点不谋而合：&lt;strong&gt;组件独立性&lt;/strong&gt;（面向对象）、&lt;strong&gt;数据管理&lt;/strong&gt;（函数式）以及功能性（结构）。&lt;/p&gt;
&lt;h2 id="再次思考"&gt;再次思考&lt;/h2&gt;
&lt;p&gt;三种编程范式都对程序员提出了新的&lt;strong&gt;限制&lt;/strong&gt;。每个范式都约束了某种编写代码的方式，没有一个编程范式是在增加新能力。也就是说，编程范式带来的是——什么不应该做。&lt;/p&gt;
&lt;p&gt;（第二篇完）&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    <item>
      <title>再读整洁架构之道（一）</title>
      <link>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%80/</link>
      <pubDate>Wed, 03 Jul 2024 23:10:20 +0800</pubDate>
      <guid>https://tommycheese.github.io/blogs/%E5%86%8D%E8%AF%BB%E6%95%B4%E6%B4%81%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93%E4%B8%80/</guid>
      <description>&lt;h2 id="前记"&gt;前记&lt;/h2&gt;
&lt;p&gt;为什么是再读？因为在这之前笔者已经阅读过一遍架构之道这本书，但当时缺乏项目的锻炼和试错，总觉得读的不深，终于有幸在工作期间接触到了一些项目和需求，更幸运的是项目组在开发时采用的架构正好是整洁架构，详细的说，是事件驱动开发DDD+整洁架构，关于DDD的详细内容将会在之后讨论，笔者就重新阅读了这本书📚，收获颇丰，因此这一个系列，我准备用自己的拙见描述一下整洁架构，更准确的说，应该是是读书笔记，以兹同仁。&lt;/p&gt;
&lt;p&gt;这本书的详细地址为：&lt;a href="https://weread.qq.com/web/bookDetail/480322f072021a3248038c8"&gt;架构整洁之道-罗伯特 C. 马丁-微信读书 (qq.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本系列会以图书的章节组织作为思路，跟随作者的脚步逐渐深入，同时，由于是再读，所以不可避免的也会穿插一些图书中其他部分的内容，所以，如果你在阅读时发现了一些难于理解或者没听说过的内容，那它大概率是在之后章节中的内容。&lt;/p&gt;
&lt;p&gt;让我们开始吧。&lt;/p&gt;
&lt;h2 id="设计与架构的含义"&gt;设计与架构的含义&lt;/h2&gt;
&lt;p&gt;设计与架构，本质上并没有区别，底层设计细节和顶层架构信息共同定义了软件系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件架构的终极目标是用最小的人力成本来满足构建和维护系统的需求&lt;/strong&gt;，因此成本可以评估一个软件架构设计的优劣。&lt;/p&gt;
&lt;p&gt;没有经过设计、匆匆构建的系统可能成为&lt;strong&gt;乱麻系统&lt;/strong&gt;，乱麻系统在构建过程中被长期忽视代码质量和设计结构优化。认真设计软件架构可以在一定程度上避免系统成为乱麻系统，从而降低成本。&lt;/p&gt;
&lt;p&gt;软件开发具有两个核心特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要想跑得快，要先跑得稳；&lt;/li&gt;
&lt;li&gt;过度自信只会使得重构设计陷入和原项目一样的困局。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="两个价值维度"&gt;两个价值维度&lt;/h2&gt;
&lt;p&gt;软件系统包含两种价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;行为价值：让机器按照某种指定方式运转，给系统的使用者创造或者提高利润；&lt;/li&gt;
&lt;li&gt;架构价值：软件要够灵活，更改软件的成本要做到与需求的范畴相关，与形状无关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎么理解需求的范畴与形状？
笔者自己理解范畴即需求大致的范围和所属&lt;strong&gt;领域&lt;/strong&gt;，而形状指需求的细节内容。&lt;/p&gt;
&lt;p&gt;那么哪个价值维度更重要呢？
作者认为架构价值比行为价值更重要，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果某程序可以正常工作，但是无法修改，那么当需求变更的时候它就不再能够正常工作了，我们也无法通过修改让它能继续正常工作。因此，这个程序的价值将成为0；&lt;/li&gt;
&lt;li&gt;如果某程序目前无法正常工作，但是我们可以很容易地修改它，那么将它改好，并且随着需求变化不停地修改它，都应该是很容易的事。因此，这个程序会持续产生价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;业务部门与研发部门经常犯的共同错误是没有把真正紧急并且重要的功能和紧急但是不重要的功能分开，结果就是重要的系统架构问题让位给了不重要的系统行为功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;平衡系统架构的重要性与功能的紧急程度这件事，是软件研发人员自己的职责&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者注：同时，即便同样是研发人员，不同的小组也会存在类似问题，正如某些前端部门认为后台只需要提供一系列接口很容易，而不知道良好设计的重要性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="为好的软件架构持续斗争"&gt;为好的软件架构持续斗争✊&lt;/h3&gt;
&lt;p&gt;如果忽视软件架构的价值，系统将会变得越来越难以维护，终会有一天，系统将会变得再也无法修改。如果系统变成了这个样子，那么说明软件开发团队没有和需求方做足够的抗争，没有完成自己应尽的职责。&lt;/p&gt;

          </description>
    <category>软件架构</category><category>架构整洁之道</category></item>
    </channel>
</rss>
