19. 在代码中展现架构和领域(译)

原文:https://herbertograca.com/2019/06/05/reflecting-architecture-and-domain-in-code/

这篇文章是软件架构编年史()的一部分,这部编年史由一系列关于软件架构的文章组成。在这一系列文章中,我将写下我对软件架构的学习和思考,以及我是如何运用这些知识的。如果你阅读了这个系列中之前的文章,本篇文章的的内容将更有意义。

在创建应用的时候,让它可以工作易如反掌。要让它在处理大量数据的情况下仍然保持性能,会有点困难。但是最难的挑战是构建一个真正可以维系多年(十年、二十年甚至一百年)的应用程序。

我工作过的大多数公司都有每三到五年就重建应用的历史,有时甚至不到两年就要重建。这种做法成本极高,它将极大地影响应用程序的成功,进而极大地影响公司的成功,还会让开发人员在乱成一锅粥的代码库种凌乱,让他们想萌生辞职的想法。任何一家志向远大的正经公司,都无法承受任何经济上、时间上、声誉上、客户上、人才上的损失。

让应用程序保持可维护性的基石是让代码能够反映出架构和领域,这对防止所有棘手问题至关重要。

清晰架构是对比我经验更丰富的开发者所倡导的原则以及实践的合理解释,也是对我如何组织代码库使之反映出项目架构与领域并便于沟通的经验总结。

在我的上一篇博客译文)里,我将这些理念汇总起来并用信息图和 UML 图呈现,试图建立我思考的概念图谱。

然而,我们怎样才能把实践落实到代码中呢?

在这篇博客里,我将说明我是如何在代码中体现一个项目的结构和领域的,还将提出一个通用的结构,我认为它能帮助我们规划好可维护性。

我的两张脑图

在这个系列的前两篇文章里我介绍了两张脑图,我用它们来思考代码和组织代码仓库,至少在我脑海里是这样想的。

第一张脑图由一系列同心圆层级组成,它们最终按照业务维度的应用模块切分,形成组件。在这张图里,依赖的方向由外向内,意味着内层对外层可见,而外层对内层不可见。

第二张则是一组平面的层级,其中最上面的一层就是前面这张同心圆,下一层是组件之间共享的代码(共享内核),再下一层使是我们自己对编程语言的扩展,最下面一层则是实际使用的编程语言。这里的依赖方向是自上而下的。

体现架构的代码风格

使用体现架构的代码风格,意味着代码风格(编码规范、类/方法/变量命名约定、代码结构...)某种程度上可以和阅读代码的人交流领域和架构的设计意图。要实现体现架构的代码风格,主要有两种思路。

[…] 体现架构的代码风格能让你给代码的阅读者留下提示,帮助他们正确地推断出设计意图。
George Fairbanks

第一种思路是通过代码制品的名字(类、变量、模块...)来传达领域和架构的含义。因此,如果一个类是处理收据(Invoice)实体的仓库(Repository),我们就应该将它命名成InvoiceRepository,从这个名字我们就可以看出,它处理的是收据领域的概念,而它在架构中被当做一个仓库。这可以帮助我们理解它应该放在哪个地方,何时使用它以及如何使用它。但是,我认为代码仓库中并不是每个代码制品都需要这样做,例如,我觉得不必为每个实体(Entity)都加上后缀Entity,这样做就有些画蛇添足,徒增噪音。

[…] 代码应该体现架构。换句话说,我一看到代码,就应该能够清晰地区分出各种组件[…]
Simon Brown

第二种思路是让代码仓库中的顶级制品明确地区分出各个子域,即领域维度的模块,也就是组件。

第一种思路应该很清楚,无需赘述。但第二种思路有点儿微妙,我们得深入探讨一下。

让架构清晰的展现出来

在我的第一张图里,我们已经看到,在最粗粒度的层级上,我们只有三种不同用途的代码:

  • 用户界面,这里的代码就是为了适配某个用例的传达机制;
  • 应用核心,这里的代码就是用例和领域逻辑;
  • 基础设施,这里的代码就是为了适配应用核心所需的工具/库。

因此,在源代码的根目录下我们可以创建三个文件夹来体现这三类代码,一个文件夹对应一个类别的代码。这三个文件夹表示三个命名空间,稍后我们甚至可以创建测试来断言核心对用户界面和基础设施可见,反过来却不可见,也就是说,我们可以测试由外向内的依赖方向。

用户界面

一个 Web 企业应用通常拥有多套 API,例如,一套给客户端使用的 REST API,还有一套给第三方应用使用的 web-hook, 业务还有一套需要维护的遗留 SOAP API,或者还有一套给全新移动应用使用的 GraphQL API…

这样的应该通常还有一些 CLI 命令,用于定时作业(Cron Job)或按需的维护操作。

当然,还有普通用户可以使用的网站本身,但也许还有另一个供应用管理员使用的网站。

这些全都是同一个应用的不同视图,全都是同一个应用的不同用户界面。

实际上我们的应用可能拥有多个用户界面,其中有些还是供非人类用户(第三方应用)使用的。我们通过文件/命名空间来区分并隔离这些用户界面,来展现出这一点。

用户界面主要有三类:API、CLI 和网站。所以我们在UserInterface根命名空间里为每个类别创建一个文件夹,将不同界面的类型清晰地区分开来。

下一步,如果有必要的话,我们还可以继续深入每种类型的命名空间,再创建更细分类的用户界面的命名空间(CLI 可能不需要再细分了)。

基础设施

和用户界面一样,我们的应用使用了多种工具(库和第三方应用),例如 ORM、消息队列、SMS 提供商。

此外,上述每一种工具都可以有不同的实现。例如,考虑一家公司业务扩张到另一个国家的情况,由于价格的因素,不同的国家最好采用不同的 SMS 提供商:我们需要端口相同的适配器的不同实现,这样使用时可以互相替换。另一个例子是对数据库 Schema 进行重构或者切换数据库引擎,需要(或决定要)切换 ORM 时:我们会在应用中注入两种 ORM 适配器。

因此,在Infrastructure命名空间来说,我们先给每一种工具类型创建一个命名空间(ORM、MessageQueue、SmsClient),然后再每一种工具类型内部为每一种用到的供应商(Doctrine、Propel、MessageBird、Twilio...)的适配器在创建一个命名空间。

核心

Core命名空间下,可以按照最粗粒度的层级划分出三类代码: 组件(Component)共享内核(Shared Kernel)端口(Port)。为这三个类别创建文件夹/命名空间。

组件

Component 命名空间下,我们为每个组件创一个命名空间,然后在每个组件命名空间下,我们再分别为应用(Application)层和领域(Domain)层分别创建一个命名空间。 在 ApplicationDomain 命名空间下,我们先将全部类放在一起,随着类的数量不断增加,再来考虑必要的分组(我觉得一个文件夹下就放一个类有些矫枉过正,所以我宁愿在必要时再进行分组)。

这是我们就要考虑是按照业务主题(收据、交易...)分组还是按照技术作用(仓库、服务、值对象...)分组,但我觉得无论怎样分组影响都不大,因为这已经是整个代码组织树的叶子节点了,如果需要,在整个组织结构的最底端进行调整也很简单,不会影响代码仓库的其它部分。

端口

Infrastructure 命名空间一样,Port 命名空间里核心使用的每一种工具都有一个命名空间,核心通过这些代码才能使用底层的这些工具。

这些代码还会被适配器使用,它们的作用就是端口和真正工具之间的转换。这种形式简单得不能再简单了,端口就是一个接口,但很多时候它还需要值对象、DTO、服务、构建起、查询对象甚至是仓库。

共享内核

我们把在组件之间共享的代码放到 Shared Kernel 命名空间下。尝试了几种不同的共享内核内部结构之后,我无法找到一种适用于所有情况的结构。有些代码和Core\Component一样按组件划分很合理(例如 Entity ID 显然属于一个组件),有些代码这样划分却不合适(例如,事件可能被多个组件触发或监听)。也许要结合使用两种划分的思路。

用户区里的编程语言扩展

最后,我们还有一些自己对编程语言的扩展。这个系列中前面一篇文章已经讨论过,这些代码本可以放在编程语言中,却因为某些原因没有。比如,在 PHP 中我们可以想到的是 DateTime 类,它基于 PHP 提供的类扩展,提供了一些额外的方法。另一个例子是 UUID 类,尽管 PHP 没有提供,但是这个类天然就是纯粹的、对领域无感,因此可以在任意项目中使用,并且不依赖任何领域。

这些代码用起来和编程语言自己的提供的功能没啥区别,因此我们要完全掌控这些代码。然而,这并不是意味着我们不能使用第三方库。我们能用而且应该用,只要合理,但是这些库应该用我们自己的实现包装起来(这样的话我们可以方便的切换背后的第三方库),而应用代码应该直接使用这些包装代码。最终,这些代码可以自成项目,使用自己的 CVS 仓库,被多个项目使用。

强化架构

上述就是所有我们决定要落地的思路和方法,这需要大量投入,也不容易掌握。就算我们掌握了所有思路和方法,但我们终究还是人类,所以我们一定会犯错,我们的同事也会犯错,事情就是这样。

就像我们为了避免写代码时犯的错进入生产环境而编写测试一样,我们也必须对代码仓库的结构做点什么。

在 PHP 的世界里,我们有一个叫做 Deptrac 的小工具可以做这种检查(但我敢打保票其它编程语言也有类似的工具),这个小工具由 Sensiolabs 创建。我们可以通过一个 yaml 文件进行配置,我们可以在其中配置有哪些层级,以及层级之间有哪些依赖。然后我们使用命令行执行测试,这意味着测试可以轻松地在 CI 中执行,就像我们可以在 CI 种执行其它测试一样。(译注,对于 Java 语言来说,也有类似的工具 https://www.archunit.org/,可以用它把依赖关系的规则写成自动化测试,但是不能生成依赖图。)。

我们还可以创建依赖图,将依赖可视化地展示出来,包括那些违反实现配置好的规则集的依赖关系:

总结

应用遵循某种领域结构组成,也遵循某种技术结构(即架构)组成。这两种结构才是一个应用的与众不同之处,而不是它使用的工具、库或者传达机制。如果我们想让一个应用可以长时间的维护,这两种结构都要清晰的体现在代码仓库中,这样开发者才能知道、理解、遵循,并在需要时改进。

这种清晰度让我们可以在编码的同时理解边界,这能反过来帮助我们保持应用的模块化设计,做到高内聚低耦合。

再一次重申,之前文章里提到的这些思路和实践大多来自于远比我优秀和经验丰富的开发者。我和我在不同公司的同事们进行过反复讨论,也在企业应用代码中进行过尝试,在我参与过的项目中都能得到很好地应用。

但是,我坚信没有银弹,没有均码的鞋子,没有圣杯

本文介绍的思路和解耦可以被视为适用于大多是企业应用的通用模板,如过有必要,不要犹豫,对其进行调整。我们总是要对上下文进行评估并竭尽所能,但我希望并相信这个模板是一个不错的开始,至少值得一试。

如果你想看看实现了这个模板的 Demo 项目,我 fork 了 Symfony Demo 应用并按照上面的思路进行了重构。你可以在这里找到我的重构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容