架构即代码,是一种架构设计和治理的思想,它围绕于架构的一系列模式,将架构元素、特征进行组合与呈现,并将架构决策与设计原则等紧密的与系统相结合。
如我的上一篇文章《为“架构”再建个模:如何用代码描述软件架构?》中所说,要准确描述软件的架构是一件颇具难度的事情。仅就实现的层面来说,也已经很难通过一个标准模型来让所有人达成一致,“哦,这就是架构”。也因此,在无法定义架构的情况下,也很难无法给出一个让所有人信服的架构治理模型。毕竟:模型只有合适的,永远没有对的。
但是呢,我们(ArchGuard Team)依旧会在 ArchGuard 构建出一个架构模型,以及架构治理模型,作为推荐的 “最佳实践”。除此,我们还应该提供一种自定义企业应用架构的可能性,这就是架构即代码。面向初级架构师来说,他们只需要按照 ArchGuard 的最佳实践来实施即可;面向中高级架师,他们可以基于 ArchGuard 提供的插件化能力 + DSL 构建自己的架构体系。
所以,如你在其它系统中所看到的那样,要提供这样的能力,需要一定的编码、配置等。所以,我们就需要构建一个架构即代码的系统。那么,问题来了,即代码又是什么鬼。
在先前的一系列的代码化(https://ascode.ink/)文章中,描述了如何将软件开发完全代码化,包含了将文档、需求、设计、代码、构建、部署、运营等变成代码化。设计和实现一个领域特定语言并不难,如《领域特定语言设计技巧》一文中所描述的过程,在这个上下文之下就是:
也因此,我们将架构即代码定义为:
架构即代码,是一种架构设计和治理的思想,它围绕于架构的一系列模式,将架构元素、特征进行组合与呈现,并将架构决策与设计原则等紧密的与系统相结合。
接下来的问题就是,如何将这个理念有机的与系统结合在一起?并友好地提供这样的 API 接口(DSL)?
于是放到当前 ArchGuard 的 PoC,架构即代码的呈现方式是 “ArchDoc”,一种基于 Markdown 的交互式代码分析和治理方式。即所有的 “代码” 都通过 markdown 来管理,优点有一大堆:
唯一的缺点就是实现这样一个工具并不简单。
不过,我们已经实现了一个简单的 PoC(概念证明)版本,在这个版本里,它的特点是:
当然了,还有各种的可扩展能力(这是一个再普通不过的特点了)。
回到日常里,我们经常听架构师说,“我们的服务采用的是标准的 DDD 的分层架构”。但是,这个分层是不是诸如于 “Interface 层依赖于 application、domain、infrastructure 层” 等一系列的依赖关系?开发人员是否知道这些规则?这些都是问题。所以,一个架构即代码的系统,它应该能显式地呈现出系统中的那些隐性知识。
诸如于,我们应该将分层中的依赖关系,显式地声明写出来:
layered {
prefixId("org.archguard")
component("interface") dependentOn component("application")
组件("interface") 依赖于 组件("domain")
component("interface") dependentOn component("infrastructure")
组件("application") 依赖于 组件("domain")
组件("application") 依赖于 组件("infrastructure")
组件("domain") 依赖于 组件("infrastructure")
}
PS:请忽视上面 Kotlin 代码中的中文元素,它只是用来说明使用中文描述的可能性。毕竟,开心的话,也可以使用文言文。
结合 ArchGuard 中的 DSL 与可视化工具(这里采用的是 Mermaid.js),就能呈现我们所设计的分层架构:
再再结合一下设计的分层 Linter 工具(正在实现中):
linter('Backend').layer()
一旦分层中的依赖关系错了,就可以在持续集成中阻断这些代码的提交 —— 类似于 ArchUnit 这样的机制。稍有区别的是,你不需要将测试和代码放在代码库中,而是可以统一的去管理它们。
而对于其它一系列的更复杂的规则来说,我们可以自定义它们,并将他们与文档结合在一起。
在这种模式之下,我们还可以将文档与代码相结合 —— 前提是:我们已经编写了一系列的规则。如我们在 ArchGuard 中,针对于不同的场景编写了一系列的规则:
select *
等有了基本架构文档规范之后,我们可以规则化它们,并结合到一起。如下是一个结合 Checklist 和规则的列表示例:
- [x] 不应该存在被忽略(Ignore、Disabled)的测试用例 (#no-ignore-test)
- [ ] 允许存在重复的 assertion (#redundant-assertion)
#no-ignore-test
对应于正在实现的 ArchGuard 中的规则,而 GFM 的 Checklist 中,如果 check 了,则可以表示为开启规则;如果没有 check,则为不开启。前面的文字部分,则是对应的规则描述,与传统的 linter 相比较,略显灵活。
而不论是编写文档还是阅读文档的人,他们可以很轻松地构建起对应的上下文。
有了设计和文档之后,就需要结合到已有的代码中,让三者保持一致和准确。在我们的场景之下,就是 ArchGuard 已有的 API,它包含了:
如下是 ArchGuard 中对于 repo 设计的 DSL(基于 Kotlin),用于创建代码仓库的分析:
repos {
repo(name = "Backend", language = "Kotlin", scmUrl = "https://github.com/archguard/archguard")
repo(name = "Frontend", language = "TypeScript", scmUrl = "https://github.com/archguard/archguard-frontend")
repo(name = "Scanner", language = "Kotlin", scmUrl = "https://github.com/archguard/scanner")
}
只有三者保持了一致,我们才能确保架构的设计与实现是一致的。
从实现的层面来说,一个架构即代码系统是一个支持编排的数据系统。原因在于,我们并不想关心数据处理的过程,但是想获取数据的结果,从结果中获取洞见。正如,我们所见到的一个个大数据系统,构建了一个个的可视化能力,以祈祷从中得到洞见。
不过,和祈祷稍有不同的是,我们是带着 N% 可能性的猜想,所以叫做探索。
传统的软件开发模型是:编辑-编译-运行(edit-compile-run),这种开发模型的前提是,我们拥有足够的业务洞见。对于一个带着丰富领域知识的业务系统来说,构建这样一个系统并不是一件困难。但是,当我们缺乏足够的领域专家,我们应该如何往下走呢?复杂问题,你只能探索 (Probe) -> 感知 (Sense) -> 响应 (Respond)。
而既然我们本身和很多新生代的架构师一样,也需要探索,也需要分析,然后才是得到结论。那么,我们不妨再尝试切换一下模式。如同,我们构建 ArchGuard 的软件开发模型,也是执行-探索(execute-explore),先从分析一下系统(发布一个分析功能),再配合已有的模式,最后得到 “结论” 或者规则(再发布一个 linter 功能)。
在数据领域,这种方式相当的流行,过去人们用 IPython,现在都改用 Jupyter;另外一个类型则是类似于 RMarkdown 提供的报表式的思路。
从模式上来说,ArchGuard 更偏向于 RStudio 的模式,只是从社区的资源上来说,Jupyter 相关的实现比较多。
在我们(ArchGuard core team)的 “数次讨论” 中,最终认为 ArchGuard 是一个大数据分析,而不是简单的数据分析。原因是系统中存在大量的 bug 和大数据相关的(狗头):
也就是所谓的 ”bug 驱动的架构设计“。
除此,之后另外一个颇有意思的点是,对于更大型的系统来说,它存在大量的新的提交,又或者是新的分支。我们即需要考虑:应对持续提交的代码,构建增量分析的功能。
当我们尝试使用大数据的思路,如 MapReduce、Streaming Analysis 相关的模式来解决相关的问题时,发现它是可以 work 的不错的 —— 毕竟都是数据分析。
ArchGuard 围绕于 DSL + Kotlin REPL + 数据可视化,构建了一个可交互的架构分析与治理平台。因为还在实现中,所以叫下一代。
上文中的(https://ascode.ink/)系列中,也包含了两个架构相关的工具,一个是代码生成 DSL:Forming、另外一个则是架构守护 DSL:Guarding。两个 DSL 所做的事情是,围绕特定的规则将架构元素组合到一起,这里的架构元素。
如果没有做过,这一个过程看上去是挺麻烦的,实现上有一些颇为简单的东西可以参考(复制):
仅仅是复制那多没意思,要是能自己做做抽象,也是一种非常好玩的事情。
如上所述,在 ArchGuard 中,我们尝试以一系列的规则,构建系统的规则,而这些规则是以插件化的形式暴露的。
这就意味着,这样一个系统应该是支持自定义的插件化能力,它即可以让你:
在 ArchGuard 中还需要改进的是,提供一种元数据的能力。
从实现层面来说,为了支撑粘合的能力,我们目前计划设计了三种能力的 DSL:后端架构查询 DSL、架构 DSL、特征 DSL。
后端架构查询 DSL
类似于 LINQ (Language Integrated Query,语言集成查询)封装 CRUD 接口,以提供编译时类型检查或智能感知支持,在 Kotlin 中有诸如于:KtOrm 的形式。如:
database
.from(Employees)
.select(Employees.name)
.where { (Employees.departmentId eq 1) and (Employees.name like "%vince%") }
.forEach { row ->
println(row[Employees.name])
}
像一个编程语言编写,可以提供更友好的语法性支持。
架构 DSL
即架构描述语言(Architecture Description Language),以提供一种有效的方式来描述软件架构。
特征 DSL:分析、扫描与 Linter
即封装 ArchGuard Scanner、Analyser、Linter 等,用于构建系统所需要的基础性架构特征。
两年前,在与众多的 Thoughtworker 一起构建 Ledge 的时候,我们就一直在强调文档代码化,并提供可交互的文档环境。在 Ledge 里,你可以使用 Markdown 来绘制各类的图表,只需要借助声明图表类型,示例见:https://devops.phodal.com/helper 。
从模式上来说,ArchGuard 更像是一个 RStudio + Jupyter 的结合版,即提供了大量自定义图形 + 组件能力的 REPL。
在 REPL 上,由于我们计划使用 Kotlin 构建 DSL,所以需要寻找的是 Kotlin 的 REPL。Kotlin 官方创建的 kotlin-jupyter 便成为了一个很好的参考,可惜还没有用得上。与此同时,Kotlin 在设计初期就有了 Kotlin Scripting 的场景,所以其实 kotlin-scripting-compiler-embeddable
就能满足需求。于是,在 PoC 里,我们参考了 Apache Zeppelin 引入了 Kotlin REPL,并创建了一个 WebSocket 作为服务。
在可视化上,稍微复杂一些,需要构建一个 Markdown 解析器、Block 编辑器等。我们暂时采用了 Mermaid.js 作为可视化的图形库之一,另外的还有 D3.js、Echarts 也是其中之一。剩下的问题,便是如何通过 DSL 来整合它们?构建前后端的数据模型是一个临时的方案?
PoC 示例见截图:
在这里,ArchGuard 的交互性分析,依然只是一个 PoC(概念证明),但是在不远的将来,你就可以在 ArchGuard 中使用它了。
构建一个这样复杂的工具,并不是一件容易的事。欢迎加入 ArchGuard,一起学习架构和架构治理,还有开发一个纯技术驱动的开源软件。
如果你想实践以下的技术,手把手教你学会:
当然,如果你也感兴趣于:
虽然,我们无法向你提供 code,但是说不定未来可以呢 —— 作为一个开源的自研项目,万一能构建开放式的商业模式?除此,你可以 show your code in public,认更多的行业上的架构师认识你。
围观我的Github Idea墙, 也许,你会遇到心仪的项目