Clean Architecture

Posted hanruikai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Clean Architecture相关的知识,希望对你有一定的参考价值。

两个价值维度

对于每个软件系统,我们都对以通过行为和架构两个维度来休现它的实际价值。软件研发人员应该确保自己的系统在这两个维度上的实际价值都能长时间维持在很高的状态。不幸的是,他们往往只关注一个维度,而忽视了另外一个维度。更不幸的是,他们常常关注的还是错误的维度,这导致了系统的价值最终趋降为零。

行为价值

软件系统的行为是其最直观的价值维度。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润。

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

架构价值

软件系统的第二个价值维度,就体现在软件这个英文单词上:software。“ware” 的意思是“产品”,而 “soft” 的意思,不言而喻,是指软件的灵活性。

 

软件系统的第一个价值维度:系统行为,是紧急的,但是并不总是特别重要。

软件系统的第二个价值维度:系统架构,是重要的,但是并不总是特别紧急。

编程范式

 

结构化编程

结构化编程是第一个普遍被采用的编程范式(但是却不是第一个被提出的),由 Edsger Wybe Dijkstra 于 1968 年最先提出。与此同时,Dijkstra 还论证了使用 goto 这样的无限制跳转语句将会损害程序的整体结构。接下来的章节我们还会说到,二是这位 Dijkstra 最先主张用我们现在熟知的 if/then/else 语句和 do/while/until 语句来代替跳转语句的。

结构化编程对程序控制权的直接转移进行了限制和规范。

 

面向对象编程

事实上,这个编程范式的提出比结构化编程还早了两年,是在 1966 年由 Ole Johan Dahl 和 Kriste Nygaard 在论文中总结归纳出来的。这两个程序员注意到在 ALGOL 语言中. 函数调用堆栈(call stack frame)可以被挪到堆内存区域里,这样函数定义的本地变量就可以在函数返回之后继续存在。这个函数就成为了一个类(class)的构造函数,而它所定义的本地变量就是类的成员变量,构造函数定义的嵌套函数就成为了成员方法(method)。这样一来,我们就可以利用多态(polymorphism)来限制用户对函数指针的使用。

面向对象编程对程序控制权的间接转移进行了限制和规范。

函数式编程

尽管第三个编程范式是近些年才刚刚开始被采用的,但它其实是三个范式中最先被发明的。事实上,函数式编程概念是基于与阿兰·图灵同时代的数学家 Alonzo Church 在 1936 年发明的入演算的直接衍生物。1958 年 John Mccarthy 利用其作为基础发明了 LISP 语言。众所周知,λ 演算法的一个核心思想是不可变性——某个符号所对应的值是永远不变的,所以从理论上来说,函数式编程语言中应该是没有赋值语句的。大部分函数式编程语言只允许在非常严格的限制条件下,才可以更改某个变量的值。

函数式编程对程序中的赋值进行了限制和规范。

 

大家可能会问,这些编程范式的历史知识与软件架构有关系吗?当然有,而目关系相当密切。譬如说,多态是我们跨越架构边界的手段,函数式编程是我们规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块的算法实现基础。

这和软件架构的三大关注重点不谋而合:功能性、组件独立性以及数据管理。

依赖反转

通过利用面向编程语言所提供的这种安全便利的多态实现,无论我们面对怎样的源代码级别的依赖关系,都可以将其反转。

通过这种方法,软件架构师可以完全控制采用了面向对象这种编程方式的系统中所有的源代码依赖关系,而不再受到系统控制流的限制。不管哪个模块调用或者被调用,软件架构师都可以随意更改源代码依赖关系。

 

面向对象编程到底是什么?业界在这个问题上存在着很多不同的说法和意见。然而对一个软件架构师来说,其含义应该是非常明确的:面向对象编程就是以对象为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可被编译成插件,实现独立于高层组件的开发和部署。

 

设计原则

 

  • SRP:单一职责原则。 该设计原则是某于康威圧律(Conway's Law)的一个推论——一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。
  • OCP:开闭原则。 该设计原则是由 Bertrand Meyer 在 20 世纪 80 年代大力推广的,其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
  • LSP:里氏替换原则。 该设计原则是 Barbara Liskov 在 1988 年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
  • ISP:接口隔离原则。 这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖。
  • DIP:依赖反转原则。 该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。

组件构建原则

 

REP:复用/发布等同原则

 

REP 原则初看起来好像是不言自明的。毕竟如果想要复用某个软件组件的话,一般就必须要求该组件的开发由某种发布流程来驱动,并且有明确的发布版本号。

 

CCP:共同闭包原则

 

这其实是 SRP 原则在组件层面上的再度阐述。正如 SRP 原则中提到的“一个类不应该同时存在着多个变更原因”一样,CCP 原则也认为一个组件不应该同时存在着多个变更原因。

 

CRP:共同复用原则

通常情况下,类很少会被单独复用。更常见的情况是多个类同时作为某个可复用的抽象定义被共同复用。CRP 原则指导我们将这些类放在同一个组件中,而在这样的组件中,我们应该预见到会存在着许多互相依赖的类。

一个简单的例子就是容器类与其相关的遍历器类,这些类之间通常是紧密相关的,一般会被共同复用,因此应该被放置在同一个组件中

 

一个优秀的软件架构师应该致力于最大化可选项数量。

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

我的心得:

高层策略与底层实现隔离,比如SPI机制。

依赖抽象不依赖具体细节实现

将系统设计为插件式架构,就等于构建起了一面变更无法逾越的防火墙

底层具体实现依赖高层抽象;

系统划分为核心子域和通用子域、支撑子域;

面向用户case设计架构,不考虑技术框架;

源码层面的依赖关系一定要指向同心圆的内侧。层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多。最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。

 

软件架构

 

独立性

 

划分边界

简单来说,通过划清边界,我们可以推迟和延后一些细节性的决策,这最终会为我们节省大量的时间、避免大量的问题。这就是一个设计良好的架构所应该带来的助益。

插件式架构

将系统设计为插件式架构,就等于构建起了一面变更无法逾越的防火墙,换句话说,只要 GUI 是以插件形式插入系统的业务逻辑中的,那么 GUI 这边所发生的变更就不会影响系统的业务逻辑。

 

为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。

其实,这也是一种对依赖反转原则(DIP)和稳定抽象原则(SAP)的具体应用,依赖箭头应该由底层具体实现细节指向高层抽象的方向。

 

策略与层次

架构设计的工作常常需要将组件重排组合成为一个有向无环图。图中的每一个节点代表的是一个拥有相同层次策略的组件,每一条单向链接都代表了一种组件之间的依赖关系,它们将不同级别的组件链接起来。

这里提到的依赖关系是源码层次上的、编译期的依赖关系。这在 Java 语言中就是指 import 语句,在 C# 语言中就是指 using 语句,在 Ruby 语言中就是指 require 语句。这里的依赖关系都是在编译过程中所必需的。

在一个设计良好的架构中,依赖关系的方向通常取决于它们所关联的组件层次。一般来说,低层组件被设计为依赖于高层组件

 

业务逻辑

业务逻辑是一个软件系统存在的意义,它们属于核心功能,是系统用来赚钱或省钱的那部分代码,是整个系统中的皇冠明珠

这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。

 

Web是实现细节

然而,Web 技术事实上并没有改变任何东西,或者说它也没有能力改变任何东西。这一次 Web 热潮只是软件行业从 1960 年来经历的数次震荡中的一次。这些振荡一会儿将全部计算资源集中在中央服务器上,一会儿又将计算资源分散到各个终端上

事实上,在过去十年内,或者说自 Web 技术被普遍应用以来,这样的振荡也发生了几次。一开始我们以为计算资源应该集中在服务器集群中,浏览器应该保持简单。但随后我们又开始在浏览器中引入 Applets-再后来我们又改了主意,发明了 Web 2.0,用 Ajax 和 javascript 将很多计算过程挪回浏览器中。我们先是非常兴奋地将整个应用程序挪到浏览器去执行,后来又非常开心地采用 Node 技术将那些 JavaScript 代码挪回服务器上执行。

框架是实现细节

我们可以使用框架——但要时刻警惕,别被它拖住。我们应该将框架作为架构最外圈的一个实现细节来使用,不要让它们进入内圈。

如果框架要求我们根据它们的基类来创建派生类,就请不要这样做!我们可以创造一些代理类,同时把这些代理类当作业务逻辑的插件来管理。

另外,不要让框架污染我们的核心代码,应该依据依赖关系原则,将它们当作核心代码的插件来管理。

 

拾遗

 

按层封装

在这种常见的分层架构中,Web 代码分为一层,业务逻辑分为一层,持久化是另外一层。换句话说,我们对代码进行了水平分层,相同类型的代码在一层。在“严格的分层架构”中,每一层只能对相邻的下层有依赖关系。在 Java 中,分层的概念通常是用包来表示的。如图 34.1 所示,所有的分层(包)之间的依赖关系都是指向下的。这里包括了以下 Java 类。

  • OrdersController:Web 控制器,类似 Spring MVC 控制器,负责处理 Web 请求。
  • OrderService:定义订单相关业务逻辑的接口。
  • OrderServicelmpl:Order 服务的具体实现
  • OrdersRepository:定义如何访问订单持久信息的接口。
  • JdbcOrderRepository:持久信息访问接口的实现。

按功能封装

另外一种组织代码的形式是“按功能封装”,即垂直切分,根据相关的功能、业务概念或者聚合根(领域驱动设计原则中的术语)来切分。在常见的实现中,所有的类型都会放在一个相同的包中,以业务概念来命名。

 

端口适配器模式

https://github.com/hanxiansen/Clean-Architecture-zh/blob/master/docs/ch34.md

https://github.com/hanxiansen/Clean-Architecture-zh/blob/master/docs/ch19.md

 

 

六边形架构代码实例

https://blog.allegro.tech/2020/05/hexagonal-architecture-by-example.html

 

以上是关于Clean Architecture的主要内容,如果未能解决你的问题,请参考以下文章

Android Clean Architecture中的登录流程

超实用教程!一探Golang怎样践行Clean Architecture?

android clean architecture 已停止运行啥意思

使用Android MVP Clean Architecture实现交互者

Clean Architecture

Clean Architecture