架构守护代码化,即使用易于阅读和维护的领域特定语言,来描述软件架构守护的规则,对诸如于分层架构、包访问规则、包数量、继承命名等进行限制。
PS:我们这里所说的代码化,所指的是与领域特定语言的方式进行描述。
早先呢,我只是因为使用 Java 编写的 ArchUnit 不支持其它语言,而在其它语言的生态里呢,也没有这样的合适的工具。所以呢,我就想着在 Uncode 里设计一个全新的架构守护工具,也就是 Inherd 开源小组里的 Guarding:https://github.com/inherd/guarding/,一个多语言的架构守护工具 —— 基于 Tree Sitter 解析各类编程语言。它设计了一套外部 DSL,其借鉴于 ArchUnit 设计的内部 DSL 语法。
观察软件架构在开发过程中的变化, 是一件非常有意思的事情。日常,我经常与中大型规模公司的架构师、技术负责人聊天,讨论一些关于架构和规范相关的问题,也是颇有意思的。当我们聊到架构的时候,从聊天的过程来看,都是颇为美好的。但是呢,打开代码库,看到代码的分层实现、代码的一些规范,我们就会发现结果并非如此。
美好的开始。系统在设计的初期,架构师们都根据了自己的能力和经验,对系统进行了快速的“精心”的设计。随后,在迭代的开发中,按自己新捕获到的知识,对系统进行一些调整。如果这些资深的架构师都在编码(间歇性的),又或者是经常性的打开代码库瞧一瞧。那么,自然而然地系统不会出现过大的偏差。所以,我坚信对于有一定规模的软件组织来说,他们对系统都是有着良好的设计。
腐化的架构。系统在中后期开发的过程中,先前的架构师缺乏对于架构的关注,又或者是经历了一些人员的变更,导致了系统出现了一处又一处的架构不一致。当然,其中还有一类典型的原因是,架构相关的文档和规范缺乏了维护。由于这一系列的种种原因,使得我们看到的系统架构与这些架构师原先的预期是不一致的。
基于上述的种种原因,在架构上实施守护便成为诸多架构师要考虑的问题。
程序员讨论写文档,也讨厌别人没写文档。
对于架构知识的记载、传播和转换,也是知识传递的范畴。从当前阶段来看,它存在以下几个不同的级别:
上述的几点,我想不论是开发者,还是架构师都是深有体会的。
文档化的架构,需要阅读和牢记
在架构设计初期,开发人员对于架构的设计往往都是经历过激励的讨论。所以,每个人对于架构的形态都掌握得差不多,不需要过多的记录也能知晓设计。沉淀下来的文案往往只是一些决策的结果,缺乏过程式的讨论等。
因为这一种种原因,所以在过去的几年里,我们一直在推崇『架构决策记录』(ADR),记录每一项架构上下文、决策和结果等相关的信息。
架构测试的局限性
这是一个老生常谈的问题,所以诸如于在 Java 世界里,人们设计出了 ArchUnit 这样在的工具来守护系统的架构。架构测试作为架构的文档,缺少易读性等等的问题。为了在多个项目中使用,还需要大量地复制和粘贴。
PS:在早期,我也尝试为 JavaScript / TypeScript 世界,设计类似的架构守护工具(即 dilay),但前端世界对于这一类的需要并不迫切。多年后,我又设计了一个新的工具,只是它已经适用于多个语言和框架。
架构守护代码化,即使用易于阅读和维护的领域特定语言,来描述软件架构守护的规则,对诸如于分层架构、包访问规则、包数量、继承命名等进行限制。
一个好的架构文档是个测试,并且可以执行。如 ArchUnit 设计的内部 DSL 语法:
classes().that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo")
这句话里,描述了一个规则: Foo
开头的类应该放在 com.foo
包下。这也是我们在设计架构的时候,会设计的架构文档。如果我们把它翻译成英语的话,它应该就是:
class(startsWith "Foo")) resideIn "com.foo"
另外一种潜在的形式可以是:
(startsWith "Foo").class resideIn "com.foo"
从实现难度上来说,两者的差别并不大,但是显然前者更易于理解和编写。以此类推,我们可以继续设计一系列的规则。
欢迎加入 Inherd Guarding 架构测试与守护。
GitHub: https://github.com/inherd/guarding 。