站内链接:

整洁架构

定义和规则

  1. 定义

整洁架构(Clean Architecture):整洁架构(Clean Architecture)是由 Robert C. Martin 提出的一种软件设计架构,它强调将软件系统分解为独立的层次和组件,以便于维护、测试和部署。整洁架构的核心思想是将系统分解为多个层次,每个层次都有明确的职责和依赖关系,从而实现高内聚、低耦合的设计。整洁架构将系统分为如下几个层次:

  • 实体层(Entity Layer):Enterprise wide business rules,实体层包含了应用程序的核心业务逻辑和数据模型,它们是应用程序中最基本的构件。实体层定义了应用程序中的数据结构,以及业务逻辑的校验和处理规则。
  • 用例层(Use Case Layer):application specific business rules,用例层包含了应用程序的业务逻辑,它们定义了应用程序的行为和操作。用例层负责处理用户请求,并协调各个领域对象之间的交互,从而实现应用程序的业务逻辑。
  • 接口适配层(Interface Adapter Layer):接口适配层负责将用例层和外部世界进行交互。它们包含了视图层(View)、控制器(Controller)、适配器(Adapter)等组件,负责将外部请求转换为用例层能够处理的格式,同时将用例层的响应转换为外部世界能够理解的格式。
  • 框架和驱动层(Framework and Driver Layer):框架和驱动层包含了应用程序的基础设施和外部环境。它们包含了数据库、Web 服务器、消息队列等组件,负责将应用程序和外部环境进行交互。该层代码一般使用胶水代码粘合第三方库和 adapter 层即可。

这四层结构组成一个同心圆:

archiecture-clean

其中内层的层次依赖于外层的层次,而外层的层次对内层的层次一无所知,这种从外到内的调用关系和 MVC 的从上到下关系是不一样的。这种关系保证了系统的稳定性和灵活性,使得内层的核心业务逻辑可以独立于外部技术的变化。

  1. 规则

在整洁架构中关键的是依赖规则,外部依赖内部,越往内层走,抽象程度越高,越能看到 policies-规则;越往外层走,细节越多,都是实现规则的 mechanisms-机制。

用例和实体

  1. 业务实体即核心业务逻辑,其封装了最普通的高级别业务规则
  2. 用例即面向用户的哪些服务(业务应用),一个用例表示某一个具体的需求实例

Entity 和 User case 组成了业务领域层,也就是通过领域模型形成的业务代码的实现

适配层

整洁架构的精华在中间的适配层,通过该层将核心的业务代码和外围的技术框架进行了解耦。从数据的角度看,适配器相当于一个数据转换器,从用例和实体使用的数据格式转换到持久层框架使用的数据。在该层次,一些 GUI 的 MVC 结构、视图、控制器可以放在该层次。

如何设计适配层,让业务代码与技术框架解耦,让业务开发团队与技术架构团队各自独立地工作,成了整洁架构落地的核心。

框架和驱动

最外层包含各种技术框架和工具,例如数据库,web 框架等,例如:

  • UI 界面的交互框架
  • 客户端和服务器的交互逻辑
  • 硬件设备的交互
  • 数据库的交互

在该层次一般会编写胶水性质的代码并和内存进行粘结通讯,该层也是大部分具体用户业务细节所在,这些业务细节和内层的核心业务细节是拆分开来的,为何?

  • 核心业务一般不会做改动,可以足够抽象和隐藏
  • 用户业务则会经常变动,并且实现的细节是多变和多样的

从部门角度来考虑,让业务代码和技术框架解耦,让业务开发团队和技术架构团队各自独立工作,这是整洁架构落地的核心,这里可能还涉及核心业务和用户业务的隔离,按这个逻辑继续演进就是后面的微服务架构。

架构权衡

架构 trade-off(也称为架构权衡)是指在设计和实现软件系统架构时,需要在不同的设计目标之间进行权衡,以达到最优的方案。借用大家都熟悉的古人的话来说呢,有点像是鱼和熊掌不可兼得

软件设计都是在尽可能满足(特定)需求的情况下,对各种方案、成本等进行权衡后做出的选择,例如成本与效益之间的取舍,效率与质量之间的权衡,时间与空间之间的考量,等等,这就是所谓的 trade-off 。一般从如下几个方面来权衡架构:

  • 性能与可扩展性:在设计架构时需要考虑系统的性能需求和预期负载,选择合适的技术和架构模式来提供足够的性能和可扩展性。
  • 可维护性与灵活性:一个良好的架构应该易于理解、修改和扩展,以满足系统的演化和变化需求。但在增加灵活性的同时可能会增加复杂性,需要在可维护性和灵活性之间进行权衡。
  • 安全性与易用性:安全性是很多应用程序的重要关注点,但过于严格的安全措施可能会降低用户的易用性。在设计架构时需要在安全性和易用性之间找到平衡点。
    • 成本与时间:在项目中通常会有时间和预算的限制,因此需要权衡投入的成本和开发时间。一些高级别的架构决策可能需要更多的时间和资源来实现,需要评估其对项目整体目标的影响。
  • 技术限制与业务需求:有时架构设计可能受到技术限制的约束,需要在技术可行性和业务需求之间进行权衡。

在进行架构权衡时,需要全面了解系统的需求、约束和优先级,并结合团队的经验和专业知识来做出决策。

人生也是一直不停的做trade-off架构 trade-off

架构分类

MVC 架构

MVC(Model-View-Controller)是一种常见的软件架构模式,用于组织应用程序的代码和逻辑。它将应用程序划分为三个主要部分:模型(Model)、视图(View)和控制器(Controller),各部分之间通过定义的接口进行交互。以下是 MVC 架构中各部分的功能和职责:

  1. 模型(Model),负责存储系统的中心数据:
    • 模型代表应用程序的数据和业务逻辑。
    • 它负责处理数据的读取、写入、验证和处理。
    • 模型通常包含数据对象、数据库操作、业务规则等。
    • 模型不依赖于视图和控制器,独立于展示和用户交互的部分。
  2. 视图(View),将信息显示给用户:
    • 视图负责应用程序的用户界面和数据展示。
    • 它将模型中的数据呈现给用户,以可视化的方式展示。
    • 视图可以是 Web 页面、UI 控件、报表等。
    • 视图不负责数据的处理和业务逻辑,仅关注数据的展示和用户交互。
  3. 控制器(Controller),处理用户输入的信息:
    • 控制器负责处理用户的输入和请求,并作出相应的响应。
    • 它接收用户的操作,调用模型进行数据处理,并将结果传递给视图进行展示。
    • 控制器负责协调模型和视图之间的交互,将用户请求映射到相应的模型操作和视图展示。
    • 控制器通常包含路由、请求处理、参数解析等逻辑。

这三个部分的交互逻辑如下:

struct-mvc

MVC 是一个比较古老的分层架构,其也关注边界职责、关注点分离,确保代码更加可维护、可测试、可扩展,该架构在比较简单的 CURD 场景中非常方便,其相比其他框架更加的清晰,更加的容易理解,其也是使用最为广泛的架构:

  • 服务器 MVC:Struts,Spring MVC,Flask,Django(MVT)、Ruby on Rails
  • 前端 MVC: angularjs、reactjs,Vue(MVVM)

六边形架构

  1. 定义

六边形架构(Hexagonal Architecture),也被称为端口和适配器架构(Ports and Adapters Architecture),其由 Alistair Cockburn 在 2005 年发明的,这是一种软件架构设计模式,旨在实现高内聚、低耦合的应用程序架构。它强调将核心业务逻辑与外部依赖(如数据库、UI、外部服务等)解耦,从而使系统更加灵活、可测试和可扩展。

六边形架构的核心思想:

  • 将应用程序看作一个中心的核心,被包围在各个边界(Hexagon)中。
  • 将输入和输出置于设计的边缘,其实就是关注点分离原则。

核心是应用程序的业务逻辑和规则,而边界则是与外部世界进行交互的接口。六边形架构将外部依赖物理上隔离到边界中,并通过适配器模式将其与核心进行交互,从而实现解耦。

  • Core(核心):包含应用程序的业务逻辑和规则,是整个系统的核心。它不依赖于外部依赖,只关注业务的本质。

  • Ports(端口):定义了核心与外部依赖进行通信的接口。这些接口可以是输入端口(Input Ports)和输出端口(Output Ports)。输入端口接收外部请求并将其传递给核心,输出端口将核心的结果返回给外部。

  • Adapters(适配器):实现了端口的具体逻辑,将核心与外部依赖进行连接。适配器可以是输入适配器(Input Adapter)和输出适配器(Output Adapter)。输入适配器负责将外部请求转换为核心可以处理的格式,输出适配器负责将核心的结果适配成外部依赖所需的格式。

其结构图示如下:

struct-hexagonal

通过六边形架构,应用程序的核心与外部依赖解耦,使得系统更加灵活和可扩展。核心可以独立于外部依赖进行测试,并可以轻松替换外部依赖,以适应不同的环境和需求。同时,六边形架构也提供了清晰的边界,使不同的团队可以独立开发和测试各自的边界组件。

六边形架构适用于需要处理复杂业务逻辑、需要与多个外部依赖进行交互的应用程序。它提供了一种清晰、可测试的架构方式,使系统的各个组成部分更加可维护和可扩展。

洋葱架构

洋葱架构(Onion Architecture)是一种常见的软件架构模式,旨在提高应用程序的可测试性、可维护性和可扩展性。它基于分层的思想,将应用程序分为四个层次(实际上就是整洁架构的扩展):领域服务(Domain Services)、基础服务(Infrastructure Services)和外部服务(observability services)。这些层次以类似于洋葱的方式进行嵌套,每个层次都有一个清晰的责任。

struct-onino

清晰架构

2017 年 Herberto Graca 在其《软件架构编年》史系列文章中提出清晰架构 Explicit Architecture,即将 DDD, Hexagonal, Onion, Clean, CQRS 等进行融合后的架构。

struct-explicit

该图中各个关键部分说明如下(参考整洁架构):

  • Application Core(中心红色): 业务逻辑实现,即应用核心,其中多边形的边界即表示六边形架构中的端口(入口/出口)
  • Primary/Driving Adapters:User Interface(用户界面),红色多边形外侧左半圆,即主动适配器,例如 MVC 的 Controller 等
  • Secondary/Driving Adapters: Infrastructure(基础设施),红色多边形外侧右半圆,即被动适配器,例如数据库、短信通知、MQ、搜索 ES 实现等等基础

其中应用核心又可以分为应用层和领域层:

  • Application Layer: 该层包含应用服务、CQRS 命令查询处理器、Event listener 事件监听器、接口定义等
  • Domain Layer: 领域层,包含领域服务(Domain Services)和领域模型(Domain Models)两大核心概念

架构术语

依赖注入

一般来说,A 类依赖 B 类,则其基本实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义一个服务接口
class ServiceInterface:
def perform_action(self):
pass

# 实现一个具体的服务类
class ServiceA(Service):
def perform_action(self):
print("Performing action in MyServiceA")

class ServiceB(Service):
def perform_action(self):
print("Performing action in MyServiceB")

在依赖倒置原则(DIP)中提出了自顶向下依赖的挑战,即高层模块不应该依赖于低层模块,二者都应该依赖于抽象,为了尽可能减少变化对系统产生的影响,我们会尽可能的将依赖对象抽象化,即:我们要依赖不变或稳定的元素(类、模块或层),这就是面向接口设计原则的最主要思想,高层模块对低层模块的实现一无所知,这样会带来如下好处:

  • 低层模块的细节实现可以独立变化,避免变化对高层模块产生污染
  • 在编译时,高层模块可以独立于低层模块单独存在
  • 对于高层模块而言,低层模块的实现是可替换的

那么,问题来了,如何实现依赖倒置原则呢?这里就使用到了依赖注入技术(依赖注入是一种消除高层依赖低层关系的设计模式)。

依赖注入(Dependency Injection,DI)是一种设计模式和软件开发技术,用于实现组件之间的解耦和依赖关系的管理。在依赖注入中,组件的依赖关系由外部的容器或者调用者(客户端)负责注入,而不是由组件自己创建或者管理。 依赖注入可以通过构造函数注入、属性注入和方法注入等方式实现。对于上面的示例代码,客户端 B 类在依赖的时候使用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义一个使用服务的客户端类
class ClientB:
ServiceInterface service

def __init__(self, service: Service):
""" 接收Service类型参数:通过将服务实例作为参数传递给客户端的构造函数,我们实现了依赖注入 """
self.service = service

def do_something(self):
self.service.perform_action()

# 创建服务实例
servicea = ServiceA()
# 创建客户端实例并注入服务实例
client = Client(servicea)
# 调用客户端的方法,实际执行的是注入的服务实例的方法
client.do_something()

借助于多态和反射机制,将具体的依赖转移到外部,究竟使用何种依赖由外部调用者来决定,这就是 DIP 和 DI 的完美结合。

低层模块

低层模块是软件系统中负责实现底层功能或提供基础设施的模块。它们通常是独立的、可复用的组件,用于处理底层的操作、算法、数据访问等。低层模块关注于实现细节,与特定的技术或框架密切相关。例如:

  • 数据访问层(Data Access Layer):负责与数据库或其他数据存储进行交互,执行数据的读取、写入和查询操作。
  • 网络通信模块:提供网络通信的功能,例如处理网络请求、建立连接、发送和接收数据等。
  • 文件操作模块:处理文件的读取、写入、复制、移动和删除等操作。
  • 日志模块:记录应用程序的运行日志,包括错误日志、调试日志、事件日志等。
  • 加密与安全模块:提供加密算法、数字签名、身份验证和访问控制等安全功能。
  • 缓存模块:实现数据缓存,提高数据访问的性能和响应速度。
  • 并发与线程模块:处理多线程、并发和并行的相关操作,例如线程管理、锁机制、任务调度等。
  • 操作系统接口模块:与操作系统进行交互,访问底层操作系统功能和资源。
  • 工具类模块:提供通用的工具函数和工具类,用于字符串处理、日期时间操作、类型转换等常用功能。

注意,在设计模式七大原则中有一个依赖倒置原则,其中讲述的低层模块即这里所述,若高层模块需要引用低层通用模块,可以以抽象接口的方式来实现:内聚,解耦,高层不关注低层的具体实现细节,仅通过接口来调用。

另外,注意底层和低层这两者的区别。

领域驱动设计

DDD(Domain-Driven Design):DDD 是一种软件开发方法论,关注的是领域模型的设计和领域知识的表达。它强调将业务领域的专业知识直接应用到软件设计中,以实现更好的业务建模和系统架构。DDD 提倡通过领域模型来驱动软件设计,并通过领域专家和开发团队的紧密合作来共同理解和解决业务问题。

  • 领域模型(Domain Model):领域模型是对业务领域的抽象和建模,它是对业务概念、规则和流程的具体表示。领域模型通过实体(Entity)、值对象(Value Object)、聚合(Aggregate)、领域服务(Domain Service)等概念来描述业务领域的核心概念和关系。
  • 通用语言(Ubiquitous Language):通用语言是在开发团队和业务专家之间共享的一种语言,它用于描述业务领域的概念、规则和过程。通过建立共享的通用语言,可以促进开发团队和业务专家之间的沟通,减少沟通障碍和误解。
  • 领域驱动设计的战略设计(Strategic Design):战略设计关注整个领域的大局观,包括划分领域边界、定义核心子域、设计上下文边界等。战略设计旨在创建一个可扩展、可维护和具有高内聚性的领域模型。
  • 领域驱动设计的战术设计(Tactical Design):战术设计关注单个领域模型的实现细节,包括实体的行为、聚合的边界、领域服务的定义等。战术设计旨在创建一个符合领域模型和业务规则的可靠和可测试的代码结构。
  • 领域驱动设计的限界上下文(Bounded Context):限界上下文是指在领域驱动设计中,将业务领域划分为不同的上下文边界,每个上下文边界具有独立的模型、语言和约束。限界上下文的划分有助于管理复杂性,促进模块化和团队自治。

领域驱动设计通过将业务领域置于软件开发的核心,强调对业务概念和规则的深入理解和建模,以及开发团队和业务专家之间的紧密合作。这是一种强调以业务为基础的软件开发方法。

分层架构

定义和说明

分层思想或者架构是计算机编程领域最为常用的思想之一,是应用系统最常见的一种架构模式,其是一种将代码按照职责和功能划分为不同层次的思想。它的目的是将不同功能的代码组织和解耦,使得代码结构清晰、可维护性高,并且方便进行扩展和修改.

分层思想在计算机领域的很多地方涉及,它的基本思想就是: 关注点分离、划分边界,这也是比较符合人类社会组织治理的发展流程的,一个小公司逐渐发展为大公司,则必然产生各种分层以提高整个组织的稳定性和交流便捷性。

上面章节所述的几种架构都是基于分层架构演进而提出的在不同场景下的设计,比如 MVC 就适合非常简单的 CURD 业务逻辑,对于大业务逻辑就需要采用整洁架构,基于 DDD 来进行整体架构的设计。

MVC 缺点

对于大项目而言(实际上中型项目就碰到了),三层分层架构(MVC 等)之外的代码都是一个大泥球(大泥球是指一个随意化的杂乱的结构化系统,只是代码的堆砌和拼凑,往往会导致很多错误或者缺陷),即过多的业务代码导致三层结构之外的部分和 Services 将无法归类的代码放在一起统一管理。

想象一下平常项目开发中的场景:

  1. 我们将一个所有 model 代码都放在同一个模块下,当然 Service 和 View 也是类似,那么每一次增加一个新的业务,都要在这三个模块下进行新增逻辑,这是不符合开闭原则的,那么只能对新增业务逻辑进行抽象,Django 中一个 app 是一个完整代码结构的思想就是一个解决思路。
  2. 多个模块相互复用多个基础服务或设施,这些基础设施可能包括核心业务逻辑,可能包括外层框架逻辑,那么基础设置的频繁改动也是不符合开闭原则的

技术演进

为了解决上面的 MVC 缺点,提出了领域驱动设计分层思想:

  • 用户界面层(Presentation Layer):负责与用户进行交互,包括用户界面的展示、用户输入的处理等。这一层通常涉及页面的渲染、事件处理等功能。这种思想就产生了前后端分离的设计模式以及组织内部架构,影响非常深远。
  • 应用层(Application Layer):负责处理应用程序的业务逻辑,它是用户界面层和领域层之间的桥梁。这一层主要包括应用逻辑的编排、业务规则的处理等。
  • 领域层(Domain Layer):包含了系统的核心业务逻辑,主要处理业务对象、业务规则等。这一层的代码应该是独立于具体技术实现的,关注业务领域的核心概念和操作。这种思想继续演化就变为了微服务架构体系。
  • 基础设施层:负责与数据存储进行交互,包括数据库的读写、缓存的操作等。这一层主要涉及数据的持久化和访问。

所以,代码的重构不仅模块和模块之间的重新设计,在更高层面还涉及到架构的解构和重组。另外,请注意,三层结构并非一定指代的是 MVC 结构,而且 DDD(领域驱动设计)+整洁框架的思想下分层架构也有了非常大的变动。

分层思想不断演进,在以业务导向架构中,我们需要以采用水平 + 垂直架构的方式来重新划分架构,将各业务模板的代码聚合到各自的业务模板中,顺便把大量地 util 和 common 内聚到服务中。而它们都基于其它低层(并非底层)模板,然后将各种单体应用拆分到微服务(业务导向架构)中。

TODO:关于这块,后续有时间需要再梳理下。

todo

todo2

参考