架构语言
架构描述语言(ADL)是一种计算机语言,用来描述软件或系统架构。这意味着如果是技术性架构,该架构必须被清楚地传达给软件开发者。功能架构下, 该软件架构必须被清楚地传达给利益相关者和企业工程师。一些软件工程团体开发了若干 ADL,如 ACME(CMU开发),AADL(SAE标准化),C2(UCI开发), Darwin(英国伦敦帝国学院开发)和 Wright(CMU开发) 。
维基百科:架构描述语言
ADL原则上的不同之处:
- 需求语言,因为 ADL 植根于解决方案,而需要说明问题。
- 编程语言,因为 ADL 不能绑定架构抽象到具体解决方案
- 建模语言,因为 ADL 往往侧重于表现构件而不是整体行为。然而,有重点表现构件的特定域建模语言(DSML)。
问题
示例:Wright
首页:http://www.cs.cmu.edu/afs/cs/project/able/www/wright/index.html
Wright 通过为体系结构描述提供正式基础来解决这个问题。作为一种体系结构描述语言,Wright 可用于为体系结构规范提供精确、抽象的含义,并分析单个软件系统和系统系列的体系结构。 Wright 还充当了探索架构抽象本身本质的工具。特别是,关于 Wright 的工作集中在显式连接器类型的概念、架构属性自动检查的使用以及架构风格的形式化上。
Wright 示例:
Style SharedData
Connector Bogus
Role User1 = set -> User1 |~| get -> User1 |~| Tick
Role User2 = set -> User2 |~| get -> User2 |~| Tick
Glue = User1.set -> Continue [] User2.set -> Continue [] Tick
where {
Continue = User1.set -> Continue
[] User2.set -> Continue
[] User1.get -> Continue
[] User2.get -> Continue
[] Tick
}
End Style
架构语言:Fklang
如《领域特定语言设计技巧》一文中所描述的过程,在这个上下文之下就是:
- 定义呈现模式。寻找适合于呈现架构的方式,如 UML 图、依赖图、时序图等。
- 提炼领域特定名词。一系列的架构相关元素,如架构风格:微内核等、架构分层:MVC 等。
- 设计关联关系与语法。如何以自然的方式来关联这些架构元素,如关键词、解析占位符等。
- 实现语法解析。除了实现之后,另外一种还要考虑的是:如何提供更灵活的扩展能力?
- 演进语言的设计。版本迭代
Fklang 示例
过去的几个月的业余时间里,一直在设计一个名为 Fklang ( https://github.com/feakin/fklang )的架构 DSL,以 DDD(领域驱动设计)为指导思想构建,除了完成 MVP 原型的编译器与代码生成,还可以使用 Jetbrains IDE 开发(搜索 Feakin)。
首先,架构描述语言或者设计语言并不是一个新的东西,Fklang 也是旧瓶新装。我们只是按自己的理解去实现了一遍,只是在实现的过程中,我们发现:基于标准化的方法论,可以实现规模化的软件开发。为此,在开发 Fklang 的过程中,便尝试结合了 “大魔头” 的类型流(Typeflow)思想,便也以软件开发工业化作为 Fklang 的目标之一。只是呢,对于 Fklang 而言,要实现开发工业化,还需要对于基础设施做一系列抽象(后面详细展开)。这也就是为什么文章的标题是探索。
TL;DR 版本:立即开始你的吐槽之旅途:https://book.feakin.com/quick-start 。
引子 1:Fklang 缘由:ArchGuard 架构治理前移
我们开发 Fklang 的初衷是,为了实践在 ArchGuard 中定义的三态模型中的设计态。对应三态如下:
- 设计态:目标架构。通过 DSL(领域特定语言) + 架构工作台来构建 。
- 开发态:实现架构。关注于:可视化 + 自定义分析 + 架构治理。
- 运行态:运行架构。结合 APM 工具,构建完整的分析链。
在 ArchGuard 中,我们关注于对开发态的治理,而其中的手段之一是:规范工具化。规范本身是应该内建的,诸如于我们应该制定好分层架构,诸如于 DDD 分层模式。并将这个分层架构与代码实现相绑定,再结合到开发工具中。诸如于 Fklang 的 layer
分层语法便是基于这个理念设计的:
layered DDD {
dependency {
interface -> application
application -> domain
interface -> domain
application -> infrastructure
interface -> infrastructure
}
layer interface {
package: "com.example.book";
}
...
}
在与 IDE 结合的情况下,我们就能在开发的过程中,避免开发人员破坏分层架构。这便是 Fklang 的第一个设计理念:显性化意图设计。
引子 2:领域驱动设计的标准化方法
在设计 Fklang 的过程中,我们也探索了一系列的架构描述语言,它们都有自己的标准方法论。与此不同的是,我们觉得采用现行的标准化方法,才能让架构语言更容易落地。考虑到,现在更流行的架构设计方法论是 DDD 模式,在进行 DDD 建模工作坊 时,采用事件风暴或者其它方法,都是通过协作设计的方式进行的,而最后需要一个规范化的输出。
Fklang 便是承载了规范化输出部分,将图形设计代码化,将与实现代码相结合(如代码生成等)。在设计 Fklang 的 DDD 部分语法,我们参考了 ContextMapper 部分(主要也是设计不出差异),示例如下所示:
ContextMap TicketBooking {
Reservation -> Cinema;
Reservation -> Movie;
Reservation -> User;
}
Context Reservation {
Aggregate Reservation;
}
Context Cinema {
Aggregate Cinema;
}
通过 Feakin 在线工具(https://online.feakin.com/),可以将上述的 DSL 显性化出来,用于与架构师和开发人员进行交流。
PS:因为 Fklang 还没有实现完整的类型系统,所以在现在的实现是与 DDD 相绑定。
引子 3:实现细节与基础设施抽象
五年前,在编写《Serverless 应用开发指南》时,我便觉得 Serverless 对于规模型团队来说,并不是一个很好的解决方案。但是呢,它提供了一个非常好的架构思考方式:对实现细节的抽象化。所以,再回到 Bob 大叔对于《架构整洁之道》的【第 6 部分:实现细节】:
- 数据库是实现细节
- Web 是实现细节
- 应用框架是实现细节
然后呢,然后呢,我们需要一个渐进式的 Darklang(https://darklang.com/),它与框架、Web、数据库无关。我们在写代码的时候,往往只会配置过一次数据库,剩下的数据库操作可能是在删表与重建。也因此,在描述数据库时,我们要配置的应该是 env,配置怎样的数据库,怎么的 http server 等等。Fklang 示例如下:
env Local {
datasource {
driver: postgresql
host: "localhost"
port: 5432
database: "test"
}
server {
port: 9090;
}
}
既然,我们在设计阶段已经定义好了 Context、Aggregate、Entity 等等,那么这个时候它是不是就可以作为一个 Web Server 运行起来呢?如下图所示:
这便引发了我们对于软件开发工业化的思考。
软件开发工业化:定义下一代架构
虽然,现在我们并没有在 Fklang 中实现真正的软件开发工业化。但是呢,我想在这里分享一下探索过程中的一些理解:
软件开发工业化是一种批量式加工的软件模式,充分利用机器的学习能力与人工设计的智慧,以在部分工序上由机器取代人,进而实现软件开发的快速规模化。
简单来说,对于每个功能而言,开发人员接收到需求之后,只需要编写对应函数中的功能,剩下的交由 AI 自行去生成与判断。诸如于,我们要新添加一个创建待办事项的 API,那么就自动生成 Controller、Repository 的代码,开发人员只需要编写 Service 中的那个对应 createTodo
方法即可。至于重构嘛,我觉得也不需要,发现重复代码之后,AI 应该自动帮你重构。
基于这样的考虑,我们觉得实现工业化应该达到三个核心点:
- 设计与实现细节分离。只编写核心业务逻辑,无需关注于所有的输入和输出,如数据库、Web API 等。
- 全生命周期半智能化。对于每一个环节进行量化,便可以实现架构进行自调节。
- 模式内建于工具。工业化意味着标准化,也意味着知识的固化到系统中,模式是软件开发的核心知识,应该由工具来继承。
而一旦实现了架构治理的前置,我们便不再需要关注于如何治理。
设计与实现细节分离:基础设施抽象
在这一点上,我们与传统的 “甩手架构师” 是保持一样的考虑,设计与一部分的实现细节是相分离的。只是核心的差异之处在于,我们作为架构师,应该构建出基础设施抽象,并显示化出设计意图,诸如于我们前面强调的《实现细节与基础设施抽象》。
我们可以拿 Serverless 架构或者 Faas(函数即服务)作为一个参照物。在采用 FaaS 架构的模式之下,我们并不关注于基础设施,云厂商或者基础设施部门会提供弹性的架构支持。但是呢,FaaS 粒度过细,过多的进程使得我们无法接受这个成本。所以,在考虑工业化时,我们需要实现:基于微服务之上的函数即服务(FaaS on Microservices)。从模式上类似于 OSGI 模型,只是实现机制是不一样的。我们理解的类似 FaaS on Microservices 的架构,可以每个聚合(aggregate )在开发时独立运行,又可以与其他聚合一起工作。
而作为开发人员,他们不需要关注于 Web 接口与数据库接口,只需要编写核心业务逻辑即可,Controller 和 JPA 接口可以由设计生成,以达到框架、数据库与设计无关。简单来说,开发人员只需要编写处理函数就足够了,应对于 MVC 的 service 中的一个方法,处理输入并返回输出结果。
全生命周期半智能化:量化与架构自调节
我们坚信软件开发工业化另外一个点在于:基于代码模型的 AI 代码生成。
- 设计态。通过在 DSL 中写入基本的设计,来生成代码与空函数,让开发人员选择与填空。
- 开发态。结合 AI 与现有的代码库能力,来判断逻辑是否正确,并进行调整。
- 运行态。通过监测 API 的运行情况,来自动调整 DDD 中的聚合、限界上下文。
在设计 Fklang 的过程中,我们构建了一个 flow 语法,它是用来生成注释给 AI 看的:
impl UserCreated {
endpoint {
POST "/user/{id}";
}
flow {
via UserRepository::getUserById receive user: User
via UserRepository::save(user: User) receive user: User;
via Kafak send User to "user.create";
}
}
其设计思想来源于,我们日常沟通中的,你这个 API 需要先查询哪个表,再 xxx,最后再 xxx。虽然,设计得还比较粗糙,重点还在于输入和输出,在配置了分层之后,会在对应的 Controller (UserController)中插入对应的代码:
@PostMapping("/user/{id}")
public User createUser() {
// 1. get user:User from UserRepository.getUserById with ()
// 2. get user:User from UserRepository.save with (user:User)
// 3. send User from Kafak to "user.create"
}
在引入 GitHub Copilot 之后,便可以自动生成靠谱,还有不靠谱的代码。
除此,既然 AI 训练可以实现由 AI 自行调参,并发方面是不是应该自行有机器来学习和设计?而要实现自调节要做的第一件事就是量化,定义成功,以反馈驱动设计。
模式内建于工具
工业化与手工作坊的区别在于,手工作坊做出的东西品质差异比较大,好的非常好,差的非常差。而工业化,虽然比不上好的,但是至少品质如一。他们所做的事情便是,将模式化的套路内建于机器的流水线之中。对于软件开发来说,也是相似的:
- 采用诸如于 DDD 的 “标准化” 设计方法。
- 采用统一的开发语言。
- ……
这其实也是每个公司想去做提效的部分。从工业化的角度来说,这里我们认为:复用、效率与规模化是类似于 CAP 的不可能三角。在设计 Uncode 我们一直想做的事情是:将软件开发过程代码化,以实现自动化。相似的,对于工业化来说,我们也需要在现有的工具中,实现对于这些成熟模式的集成。
从另外一个角度来说,我觉得现有的 IDE 依旧有很大的改进空间。诸如于,既然大部分开发人员不用 TDD,那么 IDE 在接受了 debug 之后,是不是可以记下参数,自动生成测试?
Fklang:DDD 驱动的架构 DSL
最后,让我们简单再介绍一下 Fklang,一个由 DDD 思想驱动的架构设计语言。
核心设计理念
Fklang 的目标是:通过声明式 DSL 来绑定代码实现与架构设计,保证架构设计与实现的一致性。为此,我们有三个核心设计理念:
- 架构孪生:双态绑定。提供架构设计态与实现态的双向绑定,保证架构设计与实现的一致性。
- 显性化设计意图。将软件设计的意图化,借助于 DSL 语言的特性,将意图转换化代码。
- 类型与事件驱动。通过事件驱动的方式,将数据类型与领域事件进行绑定。
PS:详细介绍见:https://book.feakin.com/design-principles (还没写完)
大部分的内容已经在上面的内容里介绍了。回到 Fklang 中,我们面临的第一个挑战是:如何在不影响开发效率的前提下,保证架构设计与实现的一致性?对于一个架构语言来说,要让开发人员采用的一个关键点是:如何真正地提升开发效率?所以,这也就是我们依赖在探索的地方。
Fklang 的其他效能探索
基于 DDD 产物的 Mock Server。既然 Fklang 能作为 DDD 设计结果的承载物,那么考虑到 API 设计也是其中的一部分,自然而然地 Mock Server 也是可以跑起来的 —— 读取 Aggregate、Entity 等生成 API。所以,一个 Mock Server 日志示例:
fkl run --main /Volumes/source/feakin/fklang/docs/samples/impl.fkl --func mock-server
Running at http://localhost:9090 !
Routes:
http://localhost:9090/api/cinema/cinema
http://localhost:9090/api/cinema/cinema/1
http://localhost:9090/api/cinema/screeningroom
http://localhost:9090/api/cinema/screeningroom/1
http://localhost:9090/api/cinema/seat
http://localhost:9090/api/cinema/seat/1
http://localhost:9090/api/movie/movie
**基于 API 的契约测试。**相似的,我们也将 API 契约作为测试的一部分,可用于测试 API 的实现是否是正确的,如下所示:
impl UserUpdated {
endpoint {
PUT "/user/{id}";
request: UpdateUser;
response: User;
}
}
不过,现在支持最好的是 GET 请求:
[2022-11-20T08:58:39Z INFO fkl] runOpt: RunOpt { main: "/Volumes/source/feakin/fklang/docs/samples/impl.fkl", path: None, impl_name: Some("PackageJsonGet"), env: None, func_name: HttpRequest, custom_func: None }
[2022-11-20T08:58:39Z INFO fkl::builtin::funcs::http_request] headers: {"user-agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1) AppleWebKit/533.2.1 (KHTML, like Gecko) Chrome/24.0.811.0 Safari/533.2.1"}
[2022-11-20T08:58:39Z INFO fkl::builtin::funcs::http_request] Content-Type: text/plain; charset=utf-8
在未来,它也可以作为自动化测试的核心部分。