引言
我们公司的技术团队在 React 工程中引入了领域驱动设计(Domain Drive Design,DDD),这是一项令人兴奋的技术实践。本文将作为我的学习笔记,与大家分享我对领域驱动设计的理解,并探讨它相较于传统架构设计的优缺点。
在 React 应用中采用领域驱动设计是为了更好地应对复杂业务场景和提高代码的可维护性。领域驱动设计并非一蹴而就的实践,而是一个深度理论的集合。在本文中,我将分享我学习 DDD 的过程,以及我们团队在项目中如何应用这一理论,以便大家快速了解其核心概念和实践方法。
一、什么是领域驱动设计?
领域驱动设计的概念来源于 2004 年著名建模专家 Eric Evans 发表的他最具影响力的书籍:《领域驱动设计------软件核心复杂性应对之道》(Domain-Driven Design --Tackling Complexity in the Heart of Software),简称 Evans DDD。
Eric 从另一个角度让技术专家重新思考自己的工作方式:面对复杂业务的系统开发,不能上来就开始系统设计和代码实现。而是通过与领域专家一起站在解决领域问题的角度去思考和设计,构建出一套领域模型,最终将业务概念和规则映射到软件设计中。从而降低或隐藏业务的复杂性,使系统更容易扩展,以应对实际业务中的复杂和多变的问题。
这里的"领域(Domain)"是指一类事物的集合,比如我们常说的"数学领域"、"金融领域"、"通信领域"等等。这些不同的"领域"意味着"边界",也意味着某些"共性"和"特性"。"技术专家"和"领域专家"是两个不同的角色,具有不同的专业背景和职责。以下是对这两个角色的解释:
-
技术专家:
- 角色:技术专家通常是软件开发领域的专业人士,拥有深厚的编程和技术知识。他们可能是程序员、架构师、工程师等,专注于应用技术解决方案来实现软件系统。
- 职责:技术专家的主要职责是理解和应用技术,包括系统设计、编码、测试、部署等方面。他们负责确保系统的可用性、性能、安全性等技术层面的要求。
-
领域专家:
- 角色:领域专家是在特定业务领域具有深刻理解的专业人士。他们可能是业务分析师、行业专家、业务所有者等,拥有对业务过程、规则和需求的深入洞察。
- 职责:领域专家的主要职责是提供对业务领域的专业见解,帮助技术团队理解业务需求,同时协助设计合适的解决方案。他们是业务知识的保护者,有助于确保软件系统能够真正满足业务需求。
在领域驱动设计(DDD)的理念中,强调技术专家与领域专家之间的紧密合作。为了有效沟通,引入了DSL(领域专用语言),这是一种简化、特定于领域的表达方式,用于更直观地描述业务领域中的概念、规则和关系。
Eric 在考虑到系统在技术实现时与现实世界存在差异的情况下,提出了多种技术建模方案和相关的架构理念。他的目标是在技术专家和领域专家共同构建领域模型时,有意无意地引导领域专家共同构建出更符合后期技术实现的模型架构体系。
使用 DSL 有助于领域专家和技术专家共同使用一种具体、清晰的语言来表达业务需求和解决方案。通过这种共同的语言,可以避免在沟通中产生歧义,确保双方对领域模型的理解是一致的。这个共同的语言被称为"统一语言",它在领域驱动设计中充当了建立共识和沟通的桥梁。
为了更具体地说明这一概念,我们可以通过一个实际案例,例如"电商平台订单管理软件"项目,来展示在不同团队角色中使用统一语言的场景:
-
业务方:
- 描述: 业务方定义了订单管理需求,包括订单的创建、支付、发货、退款等流程。
- 场景: 使用统一语言,如"订单生命周期"、"支付状态"、"发货流程"等,在需求文档中清晰地描述了订单管理的核心概念。
-
产品:
- 描述: 产品经理编写产品需求文档(PRD),明确订单管理系统的功能和用户交互。
- 场景: 在 PRD 中使用统一语言,例如"订单状态"、"支付确认"等,确保产品需求的准确传达。
-
设计:
- 描述: 设计团队制定订单管理系统的界面和交互设计,以满足用户体验的要求。
- 场景: 在设计稿中采用与业务方和产品相同的术语,例如"订单详情页"、"支付按钮"等,以保持一致性。
-
技术:
- 描述: 技术团队实现订单管理系统的后端和前端功能,编写相应的代码。
- 场景: 在编写后端代码时,使用与业务方提供的领域模型一致的术语,例如"订单实体"、"支付服务"等,确保代码逻辑与业务需求一致。
通过以上案例,不同团队成员在不同阶段使用相同的统一语言,避免了因不同角色理解差异而导致的沟通问题。这种一致性的语言应用在需求文档、设计稿和代码中,帮助团队更加高效地协同工作,减少了项目开发过程中的误解和错误。
二、领域驱动设计在软件架构中的突显与前端启发
DDD 相较于传统架构设计,更加关注在软件中建模业务结构,并注重代码的表达能力。然而,其设计思想直接面向业务场景,更适用于构建具有特定业务场景的模型。技术上,领域驱动设计的实践意味着在软件中建立清晰的领域模型,包括实体、值对象、聚合根等概念,这些直接对应业务中的实体、属性和关系。这种映射不仅使我们更准确地反映业务的本质,还使代码更易于理解和维护。
- 业务模型的贴近性:领域驱动设计在解决实际业务问题方面的优势在于其强调业务模型的贴近性,使系统更容易应对业务的变化和复杂性。通过清晰定义业务概念,我们能够更轻松地跟踪业务规则,并保持系统的灵活性。这种方法不仅使代码更具可读性和表达力,而且在面对快速变化的市场需求时,也能更为迅速、可靠地适应变化。
- 后端领域的显著成果:随着技术和架构的发展,以及业务复杂度的提升,领域驱动设计再次成为软件设计领域的热点。在后端领域,DDD 在实际项目中取得了显著的成果,并促使社区涌现出大量相关文章。这些成果和讨论不仅在后端领域有所影响,也在前端领域引起了深刻的启发。
- 前端领域的深刻启发:领域驱动设计的原则和实践在前端领域同样引起深刻的启发。通过将业务模型清晰地映射到前端代码中,可以使前端开发更贴近业务需求,提高代码的可维护性和可扩展性。DDD 的统一语言和模型构建方法也在前端团队中催生了更有效的沟通和协作。
综合而言,领域驱动设计在软件架构中的应用为整个开发过程注入了更为清晰、有条理的业务思维。其在后端领域的成功经验为前端领域提供了有益的借鉴,促使前端开发者重新审视业务模型在代码中的体现方式,从而推动了更高效、更贴近业务的前端开发实践。
三、理解干净的工程架构
近年来,软件领域涌现了多种创新的架构理念。在一篇关于干净架构文章中,我们见到了一张明晰的系统架构层次图。
这种干净架构将软件系统分为四个关键层次,每个层次都肩负着独特的职责和依赖关系,形成了一个紧密结合且互相配合的整体。
-
企业业务规则层(Enterprise Business Rules Layer):
- 作用: 架构的核心,明确定义了软件系统的核心业务逻辑,包括实体和领域事件。
- 特点: 与具体的应用程序或技术无关,是软件系统的内核。
-
应用业务规则层(Application Business Rules Layer):
- 作用: 包含用例和领域服务,定义了软件系统的应用逻辑。
- 特点: 扮演协调者的角色,处理用户事件触发后的业务流程,例如处理"添加到购物车"的操作。
-
接口适配器层(Interface Adapters Layer):
- 作用: 包含控制器、呈现器、仓储、工厂和网关,负责将外部请求转换为内部用例,以及将内部用例的输出转换为外部响应。
- 特点: 分为驱动型和被驱动型适配器,用于减少系统与第三方服务代码之间的耦合度,提升系统的灵活性。
-
框架和驱动层(Frameworks & Drivers Layer):
- 作用: 位于最外层,包含设备、网络、用户界面和外部接口,提供具体的技术实现,如数据库、邮件、Web和UI等。
- 特点: 使整个架构更加具体和实用,为软件系统提供底层的支持和驱动。
理解这四个关键层次可能对一般开发人员来说有些超纲(比如我😭)。简单来说,这里的同心圆代表软件的不同领域。实现这种架构的关键是"依赖规则(The Dependency Rule)",确保依赖关系只能指向内部,内层组件无法了解外层的任何事物,包括函数、类、变量或其他命名的实体。这种约束有助于维持内外层之间的隔离,确保架构的清晰界限。
四、深入解析前端的干净架构
在这篇前端干净的架构文章中,将软件系统划分为不同的层次,根据功能部分与应用程序域的接近程度进行分离。这一架构强调了通过领域驱动设计(DDD)的方式,令不同的功能模块能够自由地组织在一起。以下是对适配器层、应用层和领域层的详细解释。
4.1 领域层
干净架构的核心是领域层,包含描述应用程序主题领域的实体和数据,以及处理数据转换的代码。这一层定义了业务的本质,是系统的核心。
领域层的特点在于其独立性。即便在技术选择上发生改变,比如从 React 转换到 Angular,或者在某个具体用例发生调整时,领域层的核心概念和数据结构都应当保持不变。领域实体的数据结构以及相关转换的本质都是与外部世界独立的,即使外部事件触发领域的转换,也不会影响其具体实现方式。
在电商应用中,领域层可能包含定义商品实体、购物车聚合、订单服务等。例如,领域层负责验证订单是否有效、计算订单总额等核心业务逻辑。
js
// 领域实体 class Product { private name: string; private price: number; constructor(name: string, price: number) { this.name = name; this.price = price; } // 其他业务方法和规则 } // 领域服务 class ProductService { // 处理验证订单是否有效的业务逻辑 validateOrder(order: any): boolean { // 领域层的验证逻辑 return /* 验证逻辑的结果 */; } // 处理计算订单总额的业务逻辑 calculateOrderTotal(order: any): number { // 领域层的计算逻辑 return /* 计算逻辑的结果 */; } } // 在应用层调用领域服务 const productService = new ProductService(); const orderInfo = /* 用户提交的订单信息 */; const isValidOrder = productService.validateOrder(orderInfo); const orderTotal = productService.calculateOrderTotal(orderInfo); // 处理验证订单和计算订单总额的结果 /* 处理结果的逻辑 */
通过清晰的分层结构,系统能够更容易维护、测试和扩展,从而提高可维护性和可测试性。
4.2 应用层
适配器层之后是应用层,负责协调应用程序的业务逻辑和数据流。应用层的目标是解耦业务规则与具体实现细节,使业务逻辑独立于框架、库和其他技术细节,同时保持对领域层和界面层的透明性。
在 DDD 中,应用层扮演着将用户输入转化为领域对象操作的桥梁的角色。它负责协调领域层的各个对象来完成用户的请求,但不包含具体的业务规则,而是委托给领域层处理。
- 处理事务。
- 接收用户输入,解析参数。
- 协调领域层的领域对象执行业务逻辑。
- 将领域对象的结果转化为适合返回给用户的格式。
在电商应用中,应用层可能涉及处理购物车逻辑、下订单、计算价格等业务流程。
js
// 应用层服务 class ShoppingCartService { // 处理用户添加商品到购物车的请求 addToCart(product: any, quantity: number): void { // 解析用户输入 const item = /* 解析用户输入的商品信息 */; // 协调领域层的购物车对象执行业务逻辑 const cart = /* 从领域层获取购物车对象 */; cart.addItem(item); // 处理事务 /* 处理相关事务的逻辑 */ // 将购物车对象的结果转化为适合返回给用户的格式 const cartSummary = cart.getSummary(); // 返回给用户 /* 返回给用户的逻辑 */ } } // 在适配器层调用应用层服务 const shoppingCartService = new ShoppingCartService(); const product = /* 用户选择的商品信息 */; const quantity = /* 用户选择的商品数量 */; shoppingCartService.addToCart(product, quantity);
4.3 适配器层
适配器层作为整体架构的最外层,其主要任务是处理内外部数据的适配工作。这包括将外部服务的 API 数据转化为符合应用程序期望的兼容 API。
适配器的存在显著降低了我们代码与第三方服务之间的紧耦合度,从而赋予系统更大的灵活性。一般而言,适配器可划分为驱动型和被动型两类。
4.3.1 驱动型适配器
在电商应用中,考虑用户在界面上点击购买按钮的情况。这时,驱动型适配器可以负责处理用户操作,将前端的购买请求数据适配为符合后端API的格式,然后通过适配器将请求发送给后端。这个适配器主动与后端基础设施交互,将前端数据转换为后端所需的格式,降低了前端与后端的紧耦合度。
js
// 驱动型适配器 class PurchaseAdapter { // 处理购买请求,将前端数据转化为后台数据格式 adaptPurchaseRequest(frontendData: any): any { // 适配逻辑,转化为后端API所需格式 const backendData = /* 适配逻辑 */; return backendData; } // 发送购买请求给后端 sendPurchaseRequest(backendData: any): Promise<any> { // 发送请求逻辑 return /* 发送请求的异步操作 */; } } // 在应用层调用适配器 const frontendData = /* 用户在界面上点击购买按钮的数据 */; const purchaseAdapter = new PurchaseAdapter(); const backendData = purchaseAdapter.adaptPurchaseRequest(frontendData); purchaseAdapter.sendPurchaseRequest(backendData).then(response => { // 处理后端返回的响应 });
4.3.2 被动型适配器
考虑一个场景,前端需要与后端服务器通信获取商品列表数据。这时,被动型适配器可以被动接收应用发出的获取商品列表消息,与后端服务器进行通信,并将响应数据转化为前端所需的数据结构。
js
// 被动型适配器 class ProductListAdapter { // 处理获取商品列表的请求,与后端服务器通信 fetchProductList(): Promise<any> { // 发送请求逻辑 return /* 发送请求的异步操作 */; } // 将后端返回的商品列表数据转化为前端数据结构 adaptProductList(response: any): any { // 适配逻辑 const frontendData = /* 适配逻辑 */; return frontendData; } } // 在应用层调用适配器 const productListAdapter = new ProductListAdapter(); productListAdapter.fetchProductList().then(response => { // 处理成前端所需的商品列表数据 const frontendData = productListAdapter.adaptProductList(response); });
五、何时考虑使用 DDD?
DDD 是一种解决复杂业务需求的软件开发方法。通过将实现与持续进化的模型相连接,能够有效应对多变的需求和复杂的业务场景。在实践中,我们发现以下情况下考虑使用 DDD 可能更为合适:
5.1 复杂的业务及领域概念
当项目涉及到复杂的业务逻辑和领域概念时,比如商品、订单、履约等,使用 DDD 的好处就显而易见。这种情况下,通过建立良好的领域模型,可以描述和解决领域中的问题。优势包括:
- 降低代码复杂度:DDD 可以帮助我们建立精确的领域模型,清晰地描述和解决领域中的问题,从而降低代码复杂度,提高代码的可读性和可维护性。
- 避免大范围调整:通过建立稳定的领域模型,可以帮助我们更轻松地适应业务变更,避免因业务变更而导致的大范围代码调整,增加了系统的灵活性和可扩展性。
- 提高可读性和可维护性:领域模型可以促进技术人员和领域专家之间的沟通和协作,形成一种通用的语言,更好地理解业务需求和实现细节。有助于开发人员快速上手并进行系统的维护和迭代。
5.2 想要复用业务逻辑
如果希望在多个端(如 Web、移动、桌面等)实现业务逻辑并实现逻辑的复用,将逻辑抽离至领域层是一种有效的方式。优势包括:
- 独立的领域层:领域层是一个独立的层,封装了核心的业务逻辑,不依赖于任何具体的技术或平台。
- 多端共享业务逻辑:通过领域层,可以快速实现业务,并在多端共享相同的业务逻辑,提高了代码的复用性和一致性。
- 方便与其他层交互:领域层方便地与其他层(如表示层、基础设施层等)进行交互,实现数据的传输和转换,确保系统的稳定性和扩展性。
5.3 需要与多个外部系统进行集成的系统
在这种系统中,DDD 可以帮助我们定义清晰的限界上下文,规范系统之间的交互和协作。优势包括:
- 定义限界上下文:DDD 可以帮助我们定义清晰的限界上下文,规范系统之间的交互和协作,避免因为不同系统的语义差异而导致的混乱和错误。
- 作为微服务划分依据:限界上下文可以作为微服务的划分依据,每个微服务都对应一个限界上下文,内部实现特定的领域逻辑,外部通过轻量级的协议进行通信,提高了系统的灵活性和可扩展性
六、前端采用 DDD 的优缺点
在现代前端应用开发中,采用领域驱动设计(DDD)的架构既带来了显著的优点,又面临着一些不可忽视的挑战。这种架构不仅仅是一种技术选择,更是一种思维方式和方法论,它旨在将软件系统的复杂性管理得井井有条。然而,尽管DDD为我们提供了强大的工具和理念,但在实践中,我们仍然需要谨慎应对其引入所带来的影响。
6.1 优点
-
独立领域
- 通过将核心功能拆分并统一维护在一个领域中,实现了独立性。
- 模块独立性减少了测试基础设施的需求,同时使得根据每个场景或用例的需求,选择合适的第三方服务或工具,而不是被固定在某一种实现方式上,从而增加了系统的可维护性和灵活性。
-
独立用例
- 扁平的代码结构使得用例易于测试和扩展,为应用层的业务逻辑提供了更大的灵活性。
- 每个使用场景和用例的独立描述,使得可以清晰地了解到每个部分所需要的功能和服务。
- 可以根据每个场景或用例的需求,选择合适的第三方服务或工具,从而提高了系统的可维护性和灵活性。
-
更清晰的业务逻辑
- 清晰划分领域、子域和限界上下文有助于指导团队成员分工协作,降低了复杂问题的解决难度。
- DDD 的设计方法鼓励开发人员更专注于业务领域的核心概念和逻辑,使得整个应用程序的业务流程更加清晰明了。
-
可替换的第三方服务
- 适配器的使用使得外部第三方服务更容易替换,保证了接口不变即可实现服务替换,从而提高了系统的稳定性。
-
更好的模块化和可维护性
- DDD 设计有助于更好地模块化应用程序,每个领域模型都可以独立开发、测试和部署,从而降低了对整个应用程序的影响。
6.2 缺点
-
学习成本
- 需要团队具备一定的技术水平和对 DDD 理念的理解,对新手而言学习曲线较陡峭。
- 需要花费更多的时间和精力来学习和适应这种新的架构风格。
-
开发周期
- 需要对现有业务进行拆解,对团队的要求较高,且需要面对较大的心智负担。
- 如果需要考虑后续功能的接入、模块交互和需求变化,则需要保证模块的可拓展性,增加了开发周期。
-
适用场景
- 干净架构并非适用于所有场景,特别是在简单业务下,效率可能较低,小项目采用干净架构设计可能显得多余,增加了上手门槛。
-
业务代码量
- 可能导致最终打包的产物体积增加,需要精心控制代码量,适当删减代码,进行代码拆分,并确保代码的简洁性和可维护性。
-
部署和维护复杂度
- 由于领域模型的相对独立性,可能增加部署和维护的复杂度,需要更多的协调和管理来确保整个应用程序的一致性和稳定性。
-
团队沟通和协作成本
- 需要团队成员之间更密切的沟通和协作,以确保对业务领域的共同理解和一致性,可能增加了团队之间的沟通成本和协作难度。
综上所述,采用领域驱动设计的架构在前端应用中带来了诸多优点,但也需要注意其潜在的成本和挑战,以及在特定场景下可能存在的不适用性。
七、实践中的问题与挑战
在近一年的使用中,我对公司当前的架构模式产生了一些疑虑。每次进行迭代开发时,我都会反思之前设计的领域数据是否合理,以及对新增业务模型的规划应该如何进行。这一系列的思考使我对 DDD 设计模式的实际效果进行了深刻的质疑和反思。
7.1 团队理解与执行
团队成员对于 DDD 设计的一些概念不理解或存在误区,我自己也不例外。在实践中,现有的设计方案执行起来总感觉有些别扭,例如某些业务的设计存在不合理之处、缺乏完备性、未充分考虑到系统的拓展性或者业务划分存在问题,导致代码变得很臃肿。这让我认识到我们在 DDD 方面的经验尚显不足。
为了克服这一挑战,我们需要加强团队内部的学习和交流,不断提升对于 DDD 概念的理解和应用技巧,同时制定和遵循更加规范化的"设计流程 "和"开发规范",确保代码质量和一致性。在后续的章节中,我将针对这两个方面进行详细的说明,并提出具体的建议和实践方法。
7.2 复杂度的界定与拆分
DDD 一直被视为解决复杂业务的有效工具,然而,在实践中,我们往往对于"复杂"的界定并不清晰。在一个项目中,可能只有少数部分真正属于复杂业务,而绝大多数则相对简单。因此,面对复杂业务时,如何进行拆分成为了关键问题。
为了解决这一挑战,我们需要进行深入的领域分析和设计,考虑到业务的边界和关联性。这意味着将复杂业务分解为更小、更易于管理和理解的部分。在这个过程中,可以采用领域驱动设计中的聚合根、实体、值对象等概念,将复杂业务领域划分为合适的模块。
每个模块都应该负责特定的业务功能或场景,并且内部进一步拆分为多个片段,每个片段负责处理特定的任务或逻辑,以进一步降低系统整体的复杂度。这样的模块化设计不仅使得业务逻辑更加清晰,而且使得系统更易于维护和扩展。
最后,我们还需要与业务领域专家密切合作,深入了解业务需求。只有确保拆分出的领域模块或模块片段符合业务实际需求,并能够满足未来的扩展和变化,我们才能真正解决复杂业务带来的挑战。
7.3 思维转变与技术实践的结合
除了团队理解与执行以及复杂度的界定之外,将领域驱动设计(DDD)的理念融入实际的技术实践中需要更深入的思考和实践。这一过程涉及到多个方面:
- 设计领域模型:DDD 强调将业务领域模型贯穿于整个软件开发过程中。因此,开发团队需要思考如何设计聚合根、实体、值对象等领域模型,以最好地反映业务需求和逻辑。这可能涉及到与业务领域专家的深入讨论和合作,以确保模型的准确性和完整性。
- 定义领域服务:领域服务是一种提供业务功能的服务,而不是基于实体的操作。在实践中,开发团队需要确定哪些业务逻辑适合封装成领域服务,并设计相应的接口和实现。这需要综合考虑业务需求、模块之间的依赖关系以及系统的整体架构。
- 处理领域事件:领域事件是指领域中发生的重要事务性事件,可以触发其他领域模块的响应。在实践中,开发团队需要设计合适的事件驱动机制,以便及时捕获和处理领域事件,并确保系统的数据一致性和业务流程的正确性。
- 持续学习和改进 :领域驱动设计是一个持续演进的过程,开发团队需要不断学习和改进。这包括参加相关的培训课程、阅读相关的书籍和文章、参与社区讨论等方式,以不断提升对领域驱动设计理念和实践的理解和应用能力。同时,通过实践中的反思和总结,及时调整和优化团队的工作流程和开发规范,以适应项目的需求和变化。
- 探索适用的技术工具和框架:在应用领域驱动设计时,选择适合的技术工具和框架至关重要。开发团队需要对当前流行的前端开发技术进行调研和评估,以确定哪些工具和框架最适合支持领域驱动设计的实践。这可能涉及到前端框架(如React、Vue.js等)、状态管理库(如Redux、Vuex等)以及其他相关工具和库的选择和集成。
通过以上措施,开发团队可以更好地将领域驱动设计的理念融入到实际的技术实践中,从而提高系统的可维护性、扩展性和适应性,满足业务的不断变化和发展。
八、总结与思考
DDD 的价值在于降低系统耦合度,从而提升系统易维护性、扩展性和测试性。通过合理层次划分,不同层次的功能模块更贴近其核心职责,激发开发人员对业务需求的高度关注,从而提高软件系统的整体质量和可维护性。
然而,我们需明确,每种方法都有其适用的场景和局限性。我们主张在选择方法时注重合适性而非追求潮流,简单胜于复杂,演化胜于一步到位。在编写代码时,始终以"易维护、易扩展、稳定、高效"为第一性原则。无论采用何种实现方式,只要团队能够通过 DDD 达成预期目标,即可视为一种成功。
深入领域驱动设计的理念后,我们发现其核心思想贯穿了一系列原则和概念,如"单一原则"、"开闭原则"、"实体"、"值对象"、"聚合"和"仓储"。这为前端领域驱动设计提供了坚实的基础。以高内聚低耦合为核心的指导性设计理念,助力开发人员构建更为清晰、灵活和可维护的前端应用。
在下一篇实践中,我们将以 React + Zustand 技术栈为例,深入探讨前端领域驱动设计的理念。通过构建一个简单的系统,我们将在实际项目中应用这些原则和概念,展示前端建模的过程,以期构建清晰、可维护的前端应用。敬请期待,让我们一同探索前端开发的精妙之处。
创作团队
作者:Brycen
校对:Wayne,Yuki