本文试图回答以下问题:
如果上网搜索系统架构图,我们会看到各种形式的架构图,没有统一的标准。有的画得很粗,主要描述的是系统和系统之间的交互方式。有的画的很细,使用 UML 画出了类图。这些设计图要么只能看到树木,不见森林,要么只见森林不见树木,中间缺乏过渡。基于这样的设计图只能回答部分软件设计问题,由于缺乏标准,除非有详细的文字说明,否则光看设计图难以理解设计意图。
UML 曾被广泛用来描述软件设计,但是由于 UML 各个类型图之间差异大,很难通过 UML 完成软件架构的分层设计。UML 也没有给出设计软件架构的系统性的方法。比如画出用例图以后,如何做下一步的设计,如果直接从用例图进入类图的详细设计,由于跨度过大,明显增加了设计难度。如果在用例图和中间增加部署图作为过渡,用例图和部署图之间也难以衔接。
UML 更像一个图集,大部分情况下设计者只能各取所需,使用某一种 UML 图对软件的某一个部分做图解。软件设计者需要一种工具能够规范 UML 在软件设计的各个层级中的使用方式,同时又能够简化 UML 的使用。UML 有 120 多种标记 (Notation),要全面掌握 UML 需要全面的学习过程。
C4 Model 是一种系统性的软件架构设计方法。它按照自上而下的顺序,把软件架构设计分成了4个部分的内容:
C4 Model 的目标是像地图一样,让软件架构设计能逐层表达从宏观到微观的设计意图,成为软件设计的地图。
下图是使用 C4 Model 设计的影院销售系统的架构。最左边的是系统上下文视图 (System Context View),放大深蓝色的“影院销售系统”框,可以看到中间的“影院销售系统”的容器视图 (Container View),点击“API”容器,可以查看最右边的“API容器”的组件视图 (Component View),最后我们还可以查看某个组件的代码图 (Code View)。这就跟地图类似,要查找天安门可以先找到北京市,然后查看西城区,然后查看长安街,最后找到天安门,一层一层从宏观到微观。
由于有这种明确的分层拆解,C4 Model 可以让团队按照 MVP 原则分步实现软件架构设计,辅助敏捷开发。比如在设计完系统上下文图以后,团队成员可以按照其中的待设计系统,得到几个容器图设计任务,然后根据容器图中容器的优先级,设计组件,以此类推。
系统上下文图要回答的问题是本系统和其他系统之间的关系,这种关系可能是本系统依赖于其他系统也可能是其他系统依赖本系统。系统上下文图是 C4 Model 中最高层级的设计图,它一般包括以下几个标记:
下图表示的是“影院销售系统”系统上下文设计,其中 Person 的标记是“观众”。“观众”通过待开发的“影院销售系统”查看影片,选座和购买电影票。“影院销售系统”通过外界的“第三方支付系统”完成收款,通过“短信系统”向“观众”发送购票短信。
很明显系统上下文图不能回答系统如何部署和运行的问题,设计者需要进一步的设计。
容器图需要回答的问题是我们准备设计的系统是如何运行和部署的。需要注意的是容器并不是指的 Docker 容器。这里的容器表示的是任何可以运行的软件组件。容器可以是:
下面是“影院销售系统”的容器图。可以看到“影院销售系统”由“微信购票小程序”、“购票网站”、“API 服务” 等 3个容器组成。“微信购票小程序”和"购票网站“都会调用 API 服务。而 API 服务会调用系统上下文图中提到的“短信服务”和“第三方支付系统”。容器图不仅仅能表现容器之间的关系,也可以表示容器和上一层组件之间的关系。这不正是我们希望看到的“局部放大”效果吗?
组件图是容器图的下一层。它要回答的问题是某个容器内部由哪些组件构成以及它们之间的关系。
下图是我们对容器图中的 "API服务"的详细设计,可以看到“API 服务”由 “Ticketing Controller”, “Seating Controller”, “Movie List Controller”, “Messaging Component” 和 “Payment Component” 等5个组件构成。"Ticketing Controller"使用 "Messaging Component"发送短信,使用 "Payment Component"完成第三方支付。"Messaging Component"会调用“短信服务”发送取票码短信,"Payment Component"会调用"第三方支付系统"完成支付。
从这个例子我们可以看到 C4 Model 可以表示各层组件之间的关系,不仅仅是上一层,充分展现微观和宏观之间的联系,表达设计意图。
很多时候,架构设计到组件图就比较完备了,如果我们需要做详细设计也可以制作代码图。C4 Model 建议直接使用 UML 类图来设计代码图,最好直接从代码中生成。由于 UML 类图已经非常完备,我在这里也不再赘述。
经过近几年的发展,Diagram as Code 也就是直接编码画图的各种工具已经非常成熟。常用的工具有 PlantUML, Mermaid等。C4 Model 也提供了开源的工具。笔者就使用了下面的代码在 VSCode 中画出了上面的架构图。读者也可以使用 https://structurizr.com/dsl 网站绘制。
workspace {
model {
v = person "观众" "购买电影票看电影的人"
ts = softwareSystem "影院销售系统" "负责提供影片排期, 选座, 售票及观影饮料和食品等" {
web = container "购票网站" "react"
app = container "微信购票小程序" "wechat" "react"
api = container "API 服务" "java" "springboot" {
movieList = component "Movie List Controller" "提供电影排期 API"
seating = component "Seating Controller" "选座 API"
ticketing = component "Ticketing Controller" "订票 API"
msg = component "Messaging Component" "短信组件"
payment = component "Payment Component" "支付组件"
}
}
p = softwareSystem "第三方支付系统" "负责在线支付, 可能是支付宝或者微信支付等" "Existing System"
sms = softwareSystem "短信系统" "订购成功后发送取票码和消费码" "Existing System"
# System context
v -> ts "查看影片, 选座, 购买电影票"
ts -> p "在线支付"
ts -> sms "发送取票码和消费码"
# Container
v -> web "查看影片排期, 购票" "https"
web -> api "call API" "json" "https"
v -> app "查看影片排期, 购票" "https"
app -> api "call API" "json" "https"
# Components
web -> movieList "查看影片排期"
web -> seating "选座"
web -> ticketing "订票"
ticketing -> msg "短信"
msg -> sms "发送短信" "https"
ticketing -> payment "支付"
payment -> p "支付" "https"
}
views {
systemContext ts "SystemContext" {
include *
autoLayout
title "影院售票系统系统层"
}
container ts {
include *
autoLayout
}
component api {
include *
autoLayout
}
styles {
element "Software System" {
background #1168bd
color #ffffff
}
element "Person" {
shape person
background #08427b
color #ffffff
}
element "Existing System" {
background #999999
color #ffffff
}
element "Container" {
background #438dd5
color #ffffff
}
}
}
}
使用代码画图的好处是,我们可以把设计图和网页合而为一。这样团队成员可以随时通过修改网页来更新架构图,再也不用担心软件架构设计只存在于架构师的 PPT 里了。如果开发者把架构代码存成文件,开发者还可以享受代码版本管理的方便之处,清晰看到架构演进的过程。
在刚使用 VSCode 画图的使用一直找不到在哪儿预览。经过一番周折才发现是在 View 上方的一个小链接,如下图所示,点击 “Show as Structurizr Diagram”:
另外要注意的是预览 (Preview) 功能时关闭的,需要进入扩展件的设置中打开才行。
C4 Model 提供了一套 4 层架构设计模型和工具。它为我们标准化架构设计工作提供了思路。对敏捷团队而言使用这一模型可以实现架构设计任务的拆分,让小组成员共同参与到架构设计中来,辅助敏捷开发。
需要指出的是, C4 Model 是一套相对简单的架构设计模型,在各层设计中还应该跟诸如“领域驱动开发 (DDD)” 这样的方法论结合起来才能保证设计质量。另一方面,C4 Model 和许多架构设计图一样只是使用连线简要表达了各个组件之间的关系,没有完整的调用流程。笔者建议在各层之间使用 UML 序列图、状态图等说明组件调用的关系,以便于开发者理解和验证组件之间的联系和边界。