设计 DSL 来描述软件架构,第 1 部分

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

软件架构定义了软件系统的不同部分以及它们如何相互关联。保持代码库与其架构蓝图相匹配对于保持复杂软件在其生命周期内的可维护性至关重要。当然,架构会随着时间的推移而发展,但是拥有一个架构并强制执行它总是比放弃保持代码组织更好。 (请参阅我最近的博客文章: 爱你的架构 )。

当涉及到以正式且可执行的方式描述您的架构时,问题就开始了。你可以写一篇很好的 Wiki 文章来描述你的系统的架构,或者用 Powerpoint 幻灯片或一组 UML 图来描述它;但这将毫无用处,因为无法以自动方式检查代码是否尊重您的体系结构。每个曾经与超过 2 个开发人员一起从事过重要项目的人都知道规则会被打破。这导致架构债务不断增加,并对软件的长期可持续性产生各种不良副作用。您还可以使用 Sonargraph 7 或类似工具来创建您的架构蓝图的图形表示。这已经好多了,因为您实际上可以在自动构建中甚至直接在 IDE 中强制执行规则。但这也意味着每个想要了解架构的人都需要工具来查看它。如果不访问该工具,您也将无法修改架构。

如果您可以将您的体系结构描述为代码,如果您有一种 DSL(领域特定语言)可以被软件架构师用来描述系统的体系结构并且具有足够的表现力和可读性以便每个开发人员,那不是很好吗能看懂吗?好吧,我们花了一段时间才想出这个想法,但现在我相信这 显着促进采用正式和可执行的软件架构规则的缺失拼图。使用它们的长期好处是可以忽略的。

在开始设计语言时,我们提出了一些基本要求:

  1. 应该可以在一组文件中描述架构。其中一些应该足够通用,以便它们可以被许多项目重复使用,例如描述系统分层的通用模板。
  2. 应该可以用几个完全独立的方面的形式来描述一个架构。例如,一个方面描述了分层,另一个方面描述了组件,第三个方面着眼于客户端和服务器逻辑的分离。
  3. 另一方面,语言也应该足够强大,能够在一个方面描述完整的架构。
  4. DSL 必须易于阅读和学习。

现在我们正在使用该语言来描述我们自己的软件的体系结构,我们发现第二点特别强大。几分钟之内,我们就能够检测到使用我们的 Sonargraph 7 基础设施不容易检测到的问题。

架构即代码

基本构建块:组件和工件

要以正式的方式描述架构,我们首先需要考虑可用于描述系统架构的基本构建块。最小的设计单元就是我们所说的 物理组件 (或简称为 组件 )。对于大多数语言(如 Java 或 C#)来说,它们只是一个源文件,对于其他语言(如 C 或 C++)来说,组件是头文件与实现头文件中声明的元素的相关源文件的组合。要定义架构,您可以将关联的组件分组到架构工件中。然后你可以将这些工件中的几个组合在一起成为更高级别的工件等等。对于每个工件,您还可以定义它们可以使用的其他工件。

要定义哪些组件属于某个工件,您需要一种独立于文件系统物理位置的方式来寻址组件。我们需要一个组件的命名方案。


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

对于内部组件(实际上属于您的项目的组件),我们使用以下命名策略:

模块/rel-path-to-project-root-dir/source-name

对于外部组件(您的项目使用的第三方组件),我们使用略有不同的策略。在这里我们可能无法访问任何源文件:

外部 [语言]/jar-or-dll-if-present/rel-path-or-namespace/typename

现在我们可以使用模式来描述组件组:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

如您所见,单个“*”匹配除斜杠以外的所有内容,“**”匹配斜杠边界。您也可以使用 '?'作为单个字符的通配符。

现在我们可以构建我们的第一个工件:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

我们将名称中带有“business”的模块“Core”中的所有组件分组到一个名为“Business”的工件中。来自 Java 运行时的反射类现在位于它们自己的名为“Reflection”的工件中。工件也可以有“排除”过滤器。它们可以帮助您使用“除此之外的所有内容”策略来描述工件的内容。排除过滤器将始终在所有包含过滤器之后应用。

接口和连接器

要定义工件之间允许的关系,使用一些简单有效的抽象会有所帮助。让我们假设每个工件都有至少一个传入和一个传出命名端口。工件可以通过将传出端口连接到另一个工件的传入端口来连接到其他工件。我们将传出端口 称为“连接器” ,将传入端口称为 “接口”。 默认情况下,每个工件总是有一个称为 “默认” 的隐式连接器和一个也称为 “默认”的隐式接口。 除非架构师重新定义,否则那些隐式端口始终包含工件中包含的所有元素。

现在让我们连接我们的工件:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

这将允许“Business”中包含的所有元素通过将“Business”的默认连接器连接到“Reflection”的默认接口来使用“Reflection”中包含的所有元素。在我们的架构 DSL 中,您还可以编写更短的代码:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

如果我们在没有明确命名连接器或接口的情况下引用工件,语言将假定您指的是默认连接器或接口。只能在连接器和接口之间建立连接。连接特性的语法如下:

连接 连接器名称 接口List

接口列表是要连接的接口的逗号分隔列表。可以省略连接器,在这种情况下将使用默认连接器。

现在让我们假设我们不希望任何人使用反射工件的“方法”类。这可以通过重新定义“Reflection”的默认接口来实现:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

这样做将使得无法从“反射”工件外部访问方法类,因为它不是任何接口的一部分。这里我们使用了一个 include all 过滤器来将“Reflection”中的所有元素添加到界面中。然后通过使用排除过滤器,我们从界面中的可访问元素集中取出“方法”。

大多数时候您不需要定义自己的连接器。仅当您想要排除正在使用的工件的某些元素以访问已使用的工件时,才需要这样做。另一方面,使用多个接口可能非常有用。但为了完整起见,让我们在“业务”中定义一个连接器:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

现在只有名称中包含“business”和“controller”的类才能访问“Reflection”。

让我们做一些更高级的事情,假设架构师想要确保“反射”只能从“业务”层中的元素使用。为了实现这一点,我们可以简单地将“Reflection”嵌套在“Business”工件中并将其隐藏起来:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

通过将嵌套工件声明为 隐藏 ,它将被排除在周围工件的默认界面之外。我们也不需要连接任何东西,因为父工件始终可以完全访问嵌套在其中的工件。通常,一个工件可以访问属于它自己的任何东西,包括嵌套的工件和不属于任何工件的所有组件。访问其他工件需要显式连接。

注意 包含模式。如果不使用 模式,属于反射的元素将无法通过“业务”定义的模式过滤器。

您还可以对工件使用 局部 修饰符。 本地 工件不会成为周围工件的默认连接器的一部分。

如果您后来发现软件的另一部分也需要访问“反射”,您有多种选择。您可以向“业务”添加一个接口来公开“反射”,或者您可以再次从中创建一个顶级工件。这是您公开它的方式:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

通过 导出, 您可以在接口中包含嵌套工件或嵌套工件的接口。现在客户端可以连接到“Business.Refl”。连接器 导出 的对应项是关键字 include 。它将包括嵌套工件或来自连接器中嵌套工件的连接器。

在那个特定的例子中,我们可以更容易地公开“反射”:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

现在乍一看有点奇怪,不是吗——同时暴露和隐藏?好吧, hidden 将从“Business”的默认界面中排除“Reflection”,而 exposed 则使其对“Business”的客户可见。现在客户端可以连接到“Business.Reflection”,这是“Business.Reflection.default”的快捷方式。如果“Reflection”有更多接口,它们也可以连接到其他接口。

这将我们带到了架构 DSL 的另一个重要方面——封装。工件仅向其客户端公开其接口或已公开工件的接口。客户端不可能连接到嵌套的工件,直到它被其周围的工件显式公开。

在这篇文章的最后,让我们看一下工件、接口和连接器的一般句法结构:


 "Core/com/hello2morrow/Main"                          // Main.java in package com.hello2morrow
"External [Java]/[Unknown]/java/lang/reflect/Method"  // The Method class from java.lang.reflection
"NHibernate/Action/SimpleAction"                      // SimpleAction.cs in subfolder of NHibernate
"External [C#]/System/System/Uri"                     // An external class from System.dll

不同部分的顺序很重要。不遵循此特定顺序将导致语法错误。

我们在 Sonargraph-Explorer 的 8.6 版本中实现了这种语言。我们正在将这种语言推广到所有其他 Sonargraph 产品变体。您可以通过获得 Sonargraph-Explorer 的 免费评估许可证 来试验该语言。

本系列的第一篇文章到此结束。我们已经介绍了 DSL 的基础知识,并将在接下来的帖子中介绍更高级的概念。请让我知道您对这种方法的看法以及如何改进它。你会在你的项目中使用它吗?