clean architect

"architect "

Posted by Hangdong on July 24, 2024

前言

我大概在刚刚开始进行软件架构设计的时候,接触到了clean architect的架构思想和方式。刚开始进行架构设计的时候,还停留在模块,分层之类的基本元素的组合上,所以当时也寻找一些业界比较好的方法论,综合实践看下,clean architect和SOLID是最佳的设计原则。 SOLID 是以下是原则的缩写:

  • S 单一职责原则
  • O 开闭原则
  • L 里氏替换原则
  • I 接口隔离原则
  • D 依赖倒置原则

正文

clean architect

在实践中,我认为践行clean architecture的关键是依赖规则。这条规则规定源代码只能向内依赖,在最里面的部分对外面一点都不知道,也就是内部不依赖外部,而外部依赖内部。这种依赖包含代码名称,或类的函数,变量或任何其他命名软件实体。 同样,在外面圈中使用的数据格式不应被内圈中使用,特别是如果这些数据格式是由外面一圈的框架生成的。我们不希望任何外圆的东西会影响内圈层。

外圈通常是应用层的最上层,比如UI层; 内圈通常是应用层的最底层,比如数据层,data center这种entities

Clean Architecture 和 MVVM

在实践中,有界面的应用因为需要设计view mode,故通常需要结合clean architecture和MVVM进行架构设计。

从整个应用程序的功能层级来看,分为UI层,业务层,数据层部分,其中数据层,业务层可以在项目间复用,UI层根据不同的项目定义,开放修改。 从依赖视角来看,UI层到业务层,业务层到数据层,层层单向依赖。 从数据视角来看,数据从底层传入后,经由数据层,业务层,UI层,层层对数据进行处理加工。

能够重用的代码(即Business Use Cases和Data层)即是整体软件架构平台层中的业务基础部分,这部分代码可以在不同的项目中复用,可以加速应用层的开发工作。

需要考虑的3个方面

  1. 功能性需求:功能模块,层次化,交互
  2. 非功能性需求: 性能, 复用, 质量
  3. 生命周期: 扩展, 重构,集成

表现类型

  • 业务逻辑架构: 业务实现,功能划分,与需求中要求的功能和业务紧密联系
  • 技术实现架构: 功能聚合,层次划分,与详设中的模块紧密联系
  • 静态架构: 层次机构, 功能模块,接口关系
  • 动态架构: 复杂功能, 状态控制, 数据流动, 中断处理,时间管理,资源管理

设计视角

  1. 架构对外:功能, 接口, 环境影响
  2. 对内: 静态结构, 动态行为, 资源配置
  3. 设计限制: 非功能性需求, 资源限制

评估准则

  1. 可行性
  2. 时间,成本
  3. 可扩展性
  4. 平台化 以上这些因素,都可以成为最终采用购买,自研开发还是重用的评估输入

测试

我认为可以通过集成测试来对架构进行闭环验证

  1. 按照架构设计定义的模块或者组件进行集成,集成到更大的软件项,直至到和架构设计完全一致的软件项
  2. 通过接口测试来验证架构设计中定义的接口
  3. 交互测试,对应架构中的动态行为
  4. 功能测试,对应架构设计中的功能模块定义和约束(在功能模块中需要明确该模块实现的功能)

多态

  1. 动态多态
  2. 静态多态:采用模板递归模式(CRTP)
  3. C++中的返回值优化(Return Value Optimization, RVO)和移动语义,避免了右值(即临时对象)复制的过程。

信息隐藏和深模块

一个关于系统分解为模块的重要错误认识

将系统分解为模块时,请尽量不要受运行时操作顺序的影响,否则您将沿着时间顺序分解的错误道路前进,这将导致信息泄露和浅模块。相反,请考虑执行应用程序的任务所需的不同知识,并在设计每个模块时封装这些知识中的一个或几个。这样将产生一个整洁和简单的深模块设计。

减少方法的一个误区

仅在每个方法的 API 都保持简单的前提下,减少方法的数量才有意义。如果您必须引入许多额外的参数才能减少方法数量,那么您可能并没有真正简化接口。

软件复杂性的来源

不管是专用的类或方法还是代码里的特殊情况,都是软件复杂性的主要来源。专用代码无法完全消除,但通过好的设计能够显著减少专用代码,并将专用代码与通用代码分开。这能使类更深、做到更好的信息隐藏以及让代码更简单、更清晰。

透传变量数据的几种方式

image

  1. 通过方法 m1 和 m2 传递,即使它们并不使用它。
  2. main 和 m3 具有对一个对象的共享访问权,因此可以将变量存储在此处,而不用将其传递给 m1 和 m2。
  3. 在(c)中,证书存储为全局变量。
  4. 在(d)中,证书与其他系统范围的信息(例如超时值和性能计数器)一起存储在上下文对象中;对上下文的引用存储在其方法需要访问它的所有对象中。

下沉复杂度

让模块的接口简单比让其实现简单更为重要。

配置参数的误区

配置参数是上升复杂性而不是下沉复杂性的一个示例。 因此,您应尽可能避免使用配置参数。在暴露配置参数之前,请问自己:“用户(或更高层级的模块)能比我们确定一个更好的参数值吗?” 当您创建配置参数时,请确认是否可以提供合理的默认值,以便用户仅需在特殊情况下提供这个值。理想情况下,每个模块都应当彻底解决问题,而配置参数使得解决方案不完整,从而增加了系统复杂性。

goto语句的误区

在switch case的场景下,case中进程有重复的代码,Goto 语句通常被认为是一个坏主意,如果不加选择地使用它们,可能会导致无法维护的代码,但是在诸如此类的情况下,它们可用于摆脱嵌套代码,因此也是有用的。

抽取方法或者类的误区

应该可以独立地理解每一个方法。如果您只能在理解一个方法的实现的前提下才能理解另一个方法的实现,那就是一个危险信号。该危险信号也可以在其他情况下发生:如果两段代码在物理上是分开的,但是只有通过查看另一段代码才能理解它们,这就是危险信号。

异常处理的误区

很多代码中的异常是没有真正运行到的代码,因为异常不经常发生,故可能导致代码的验证不充分,出现比较严重的问题。 异常处理越多越容易增加代码的复杂度

一个文件删除的例子

windows 对比 unix 总体而言,减少代码缺陷的最好方法是简化软件。 这里的简化并不是软件内部的简单,而是指软件暴露接口要简化

命名的一些志同道合

不需要包含类型信息

尽管我过去也会在变量名称中包含类型信息,但不再推荐这样做。随着现代 IDE 的出现,很容易从变量名称跳转到其声明(或者 IDE 甚至可以自动显示类型信息),因此不需要在变量名称中包含此信息。

精心选取的名称能大大提高代码的可读性。当有人第一次遇到该变量时,他们对行为的第一次猜测就是正确的,而不需要太多的思考。选取好名称是第 3 章讨论的投资思维的一个示例:如果您花一些额外的时间来选取好名称,将来您将更容易处理代码。此外,您引入代码缺陷的可能性更小。培养命名技巧也是一项投资。当您第一次决定不再满足于平庸的名称时,您会发现想出好名称的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现命名变得更加容易。最终,您将几乎不需要花费额外的时间来选取好名称,因此您几乎可以毫不费力地获得它的好处。

注释是一种设计工具

在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就缩写了描述抽象的注释,就可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要,否则,您只就是个编代码的。

注释属于代码,而不是提交日志

注释需要维护更新

最有用的注释(它们不是简单地重复代码)也最容易维护。

战略性思考

为了实现此目标,您必须抵制快速解决问题的诱惑。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会持续改善系统设计。

一致性的重要性

编程规范 如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。即能做到代码是容易理解的。

容易理解的代码

测试驱动开发的适用场景

有一个地方先编写测试是有意义的,那就是修复代码缺陷的时候。在修复一个缺陷之前,请编写一个会由于该缺陷而失败的单元测试,然后修复该缺陷并确保相应的单元测试可以通过。这是确保您已真正修复该缺陷的最佳方法。如果您在编写测试之前就已修复了该缺陷,则新的单元测试有可能实际上并不会触发该缺陷,在这种情况下,它也无法告诉您是否真的修复了该问题。

避免Getters 和 Setters的过度使用

因为这2种方法都是非常浅的接口,需要避免过多的暴露信息,无法做到隐藏模块的信息

性能优化

关键路径代码简化

设计两次 原则

复杂性

处理复杂性是软件设计中最重要的挑战

结束

好了,今天暂时更到这,欢迎大家阅读、批评和指正,下回再见。

参考: https://github.com/yingang/aposd2e-zh/blob/main/docs/ch06.md