1、领域驱动设计的代码模型
没有必要要求每个团队都遵守一套代码模型,但在同一个项目中,代码模型应作为架构规范要求每个团队成员必须遵守
在代码模型设计因素中,需要考虑层与模块之间的职责分离与松散耦合,同时还必须将整个限界上下文作为基本设计单元,照顾到限界上下文之间的协作关系。
1 | - application |
- application: 应用层,该限界上下文所有的应用服务
- interfaces:对gateways中除了persistence之外的抽象。包括访问除数据库之外其他外部资源的抽象接口,以及访问第三方服务或其他限界上下文服务的抽象接口。从分层架构的角度讲,interfaces 应该属于应用层,但在实践时,往往会遭遇领域层需要访问这些抽象接口的情形,单独分离 出 interfaces,非常有必要。
- domain: 领域层,将 repositories 单独分了出来,目的是为了更好地体现它在基础设施层扮演的与外部资源打交道的网关语义。
- repositories: 领域驱动设计中战术设计阶段的资源库,皆为抽象类型。如果该限界上下文的资源库并不复杂,可以将 repositories 合并到 domain 中。
- gateways: 对应了领域驱动设计的基础设施层,命名为 gateways,是为了更好地体现网关的语义,其下可以视外部资源的集成需求划分不同的包。其中,controllers 相对特殊,它属于对客户端提供接口的北向网关,等同于上下文映射中“开放主机服务(OHS)”的概念。这里主要体现了它的网关角色。client 包下的实现类与 interfaces 下的对应接口组合起来,等同于上下文映射中“防腐层(ACL)”的概念。
2、代码模型的架构决策
代码模型属于软件架构的一部分,它是设计模型的进化与实现,体现出了代码模块(包)的结构层次。在架构视图中,代码模型甚至会作为其中的一个视图,通过它来展现模块的划分,并定义运行时实体与执行视图建立联系,如下图所示:
在领域驱动设计背景下,代码模型的设计可以分为两个层次,具体如下。
- 系统层次:设计整个软件系统的代码模型。
- 限界上下文层次:设计限界上下文内部的代码模型。
我们可以将每个限界上下文视为一个自治单元,这个自治单元就像一个独立的子系统,可以有自己的架构。在架构设计时,需要找出那些稳定不变的本质特征,且这个特征与系统的目标还有需求是相匹配的。结合领域驱动设计,限界上下文以及上下文映射就是这样的一种抽象:
- 如果我们将限界上下文视为微服务,则该系统的架构风格就是微服务架构风格;
- 如果我们将上下文协作模式抽象为发布/订阅事件,则该系统的架构风格就是事件驱动架构风格;
- 如果在限界上下文层面将查询与命令分为两种不同的实现模式,则该系统的架构风格就是命令查询职责分离(CQRS)架构风格。
这些架构风格适应于不同的应用场景,即这些风格的选择应与系统要解决的问题域相关。为了保证整个软件系统架构设计的一致性,我们可以结合 Simon Brown 提出的 C4 模型来考虑设计元素的粒度和层次:分别为系统上下文(System Context)、容器(Containers)、组件(Components)以及类(Classes)
- 系统上下文:是最高的抽象层次,代表了能够提供价值的东西。一个系统由多个独立的容器构成。
- 容器:是指一个在其内部可以执行组件或驻留数据的东西。作为整个系统的一部分,容器通常是可执行文件,但未必是各自独立的进程。从容器的角度理解一个软件系统的关键在于,任何容器间的通信可能都需要一个远程接口。
- 组件:可以想象成一个或多个类组成的逻辑群组。组件通常由多个类在更高层次的约束下组合而成。
- 类:在一个面向对象的世界里,类是软件系统的最小结构单元。
C4 模型中的容器基本等同于微服务的概念,推而广之也就代表了限界上下文的概念。其实,容器与组件之间的边界很模糊,这取决于我们对限界上下文之间通信机制的决策。不仅限于此,即使采用了微服务架构风格,我们识别出来的限界上下文亦未必一定要部署为一个微服务。它们可能为整个系统提供公共的基础功能,因而在微服务架构中实际是以公共组件的形式而存在的。
在代码模型上,这些公共组件又分为两种。
一种公共组件具有业务价值,因而对应于一个限界上下文,可以视为是支撑子域(Supporting SubDomain)或通用子域(Generic SubDomain)在解决方案上的体现,如规则引擎、消息验证、分析算法等。那么,在微服务架构风格中,为何不将这样的限界上下文部署为微服务呢?这实际上是基于微服务的优势与不足来做出的设计决策。
如上图所示,微服务保证了技术选择的自由、发布节奏的自由、独立升级的自由以及自由扩展硬件配置资源的自由。为了获得这些自由,付出的代价自然也不少,其中就包括分布式系统固有的复杂度、数据的一致性问题以及在部署和运维时带来的挑战。除此之外,我们还需要考虑微服务协作时带来的网络传输成本。如果我们能结合具体的业务场景考虑这些优势与不足,就可以在微服务与公共组件之间做出设计权衡。
我认为当满足以下条件时,应优先考虑设计为微服务:
- 实现经常变更,导致功能需要频繁升级;
- 采用了完全不一样的技术栈;
- 对高并发与低延迟敏感,需要单独进行水平扩展;
- 是一种端对端的垂直业务体现(即需要与外部环境或资源协作)。
当满足以下条件时,应优先考虑设计为公共组件:
- 需求几乎不会变化,提供了内聚而又稳定的实现;
- 在与其进行协作时,需要传输大量的数据;
- 无需访问外部资源。
如果找不到支持微服务的绝对理由,我们应优先考虑将其设计为公共组件。
另一种公共组件并非领域的一部分,仅仅提供了公共的基础设施功能,如文件读写、FTP 传输、Telnet 通信等。本质上,这些公共组件属于基础设施层的模块。如果是多个限界上下文都需要重用的公共模块,甚至可以将该公共组件视为一种基础的平台或框架。这时,它不再属于限界上下文中的代码模型,而是作为整个系统的代码模型。当然,倘若我们将这种公共组件视为基础平台或框架,还可以为其建立单独的模块(或项目),放在专有的代码库中,并以二进制依赖的形式被当前软件系统所使用。(正如公司里面的构建的xxx Common SDK)
总体而言,一个典型的微服务架构通常如下: