DDD三难困境解析

banq

想象一下,在走钢丝时,你试图同时玩三个球。这似乎是一个极端且不可能完成的挑战,但它却完美地抓住了软件开发中一个常见问题的本质,即 DDD 三难困境。

DDD(领域驱动设计)三难困境涉及三个经常相互冲突的目标:

  1. 领域模型封装
  2. 领域模型纯度
  3. 性能

开发人员经常发现自己需要平衡这三个相互竞争的方面,力求找到一个不会为了其他方面而牺牲其中一个方面的解决方案。

在软件开发领域,领域驱动设计是一种强调技术专家和领域专家之间的协作以创建准确反映复杂业务规则的模型的方法。随着你深入研究这个领域,你会发现创建一个将所有业务逻辑封装在领域模型中、保持该模型的纯度并高效执行的解决方案并非易事。本文探讨了 DDD 三难困境的每个元素,通过引人入胜的示例说明了这些挑战,并提出了克服这些障碍的策略。

在深入研究解决方案之前,让我们先探讨一下 DDD 三难困境的每个组成部分,以了解它们为什么经常相互冲突。
1、领域模型封装(完整性)
领域模型封装是指领域模型应包含所有业务逻辑和规则。这一概念强调了隐藏实现细节并仅公开必要内容的重要性。它允许在模型周围保持清晰的边界,以便准确反映其所代表的现实世界领域。封装可确保业务逻辑的更改只需要对模型进行最少的更改,从而实现更好的维护和可扩展性。

现实世界的类比
以智能手机为例。它把复杂的操作和功能封装在一个简单的界面后面。用户不需要知道硬件如何处理触摸输入或数据如何通过网络发送。他们只需要知道如何使用应用程序。这一抽象层允许用户与手机交互,而不会被其内部工作原理所困扰。

2、领域模型纯度
领域模型纯度原则是,领域模型应不受外部关注(如数据库交互、用户界面或其他技术细节)的影响。这可确保模型专注于表示领域及其规则,并尽可能保持其干净和抽象。

现实世界的类比
想象一下,一位作家在树林里有一间专门的写作小屋。这个空间没有电视、互联网或电话等干扰。小屋让作家可以不受干扰地专注于创作故事。同样,纯领域模型就像作家的小屋,领域逻辑可以独立于技术问题而存在。

3、性能
在 DDD 中,性能是指确保应用程序高效运行并满足用户对速度和响应能力的需求。性能考虑通常需要优化技术,这些技术可能涉及缓存、延迟加载或数据库索引。然而,这些技术有时会侵入领域模型,威胁其纯度和封装性。

现实世界的类比
想象一下,一家以从零开始烹制的美食而自豪的餐厅。虽然每道菜都很美味,而且制作精良,但每道菜的等待时间都超过一小时。虽然质量很好,但性能(服务速度)却不足。同样,领域模型可以设计精美,逻辑完整,但如果性能不佳,就不会让用户满意。

应对三难困境
1、平衡封装和纯度
最大的挑战之一是保持封装性和纯度。虽然封装所有领域逻辑至关重要,但这样做时不让技术问题污染模型也同样重要。

策略:反腐败层
可以使用防腐层 (ACL)作为领域模型和外部系统之间的缓冲,防止外部模型破坏内部领域逻辑。ACL 在两个模型之间进行转换,确保领域模型保持纯粹,并且仅通过定义良好的接口与外界交互。

实现示例
假设您正在开发一个电子商务应用程序,其中域模型应该保持纯净和封装,但应用程序需要与旧库存系统交互。通过实现 ACL,域模型通过适配器与库存系统通信。此适配器将特定于域的请求转换为旧系统可以理解的术语,反之亦然,从而保持封装和纯净。

class  InventoryACL : 
    def  init ( self, legacy_inventory_system ): 
        self.legacy_inventory_system = legacy_inventory_system 
    def  check_availability ( self, product_id ): 
        将领域请求转换为遗留系统请求
        return self.legacy_inventory_system.query_availability(product_id) 
    def  update_stock ( self, product_id, quantility ): 
        将领域请求转换为遗留系统请求
        return self.legacy_inventory_system.update_stock(product_id, quantility) 
通过 ACL 交互的领域模型
class  OrderService : 
    def  init ( self, inventory_acl ): 
        self.inventory_acl = inventory_acl 
    def  place_order ( self, product_id, quantility ): 
        if self.inventory_acl.check_availability(product_id) >= quantility: 
            下订单的业务逻辑
            self.inventory_acl.update_stock(product_id, quantility) 
            return  "订单下达成功" 
        return  “库存不足”

2、平衡纯度和性能
纯度通常以牺牲性能为代价。一个完全纯粹的领域模型可能会很慢,因为它需要在各层之间进行转换,并严格遵守领域规则。

策略:CQRS(命令查询职责分离)
CQRS将读写操作分离,让领域模型在保持纯粹的同时,还能优化性能。通过划分这些职责,开发人员可以优化读取操作以提高性能,而不会影响领域模型写入操作的纯粹性。

实现示例
在财务应用程序中,您可以使用 CQRS 来处理交易(写入操作)和生成报告(读取操作)。这样,域模型就可以专注于交易逻辑,而读取操作则可以单独进行性能优化。

写操作的命令模型
class  TransactionService : 
    def  init ( self, transaction_repository ): 
        self.transaction_repository = transaction_repository 
    def  perform_transaction ( self, account_id, amount ): 
        交易的业务逻辑
        transaction = Transaction(account_id, amount) 
        self.transaction_repository.save(transaction) 

读操作的查询模型
class  ReportService : 
    def  init ( self, report_repository ): 
        self.report_repository = report_repository 
    def  generate_monthly_report ( self, account_id ): 
        针对性能的优化查询
        return self.report_repository.get_monthly_report(account_id)

封装也会影响性能,因为紧密封装的模型可能需要多层转换和验证,从而降低操作速度。

策略:领域事件
领域事件允许领域模型发布更改或重要事件,而无需直接调用外部服务。这样可以将领域逻辑与性能关键型操作分离开来,因为侦听器可以异步响应这些事件。

实现示例
假设有一个社交媒体应用程序,用户操作会触发各种系统操作。通过使用域事件,域模型可以发布“UserPostedPhoto”之类的事件,并且各种侦听器可以独立处理缓存、通知和分析。

领域模型
class  UserService : 
    def  init ( self, event_dispatcher ): 
        self.event_dispatcher = event_dispatcher 

    def  post_photo ( self, user_id, photo ): 
        发布照片的业务逻辑
        self.event_dispatcher.dispatch( "UserPostedPhoto" , { "user_id" : user_id, "photo" : photo}) 

事件监听器
def  cache_listener ( event_data ): 
    缓存逻辑
    print ( f
"为用户{event_data[ 'user_id' ]}缓存照片"

def  notification_listener ( event_data ): 
    通知逻辑
    print ( f
"为用户发布的照片​​发送通知{event_data[ 'user_id' ]} "

事件调度器
class  EventDispatcher : 
    def  init ( self ): 
        self.listeners = {} 

    def  register_listener ( self, event_name, listener ): 
        if event_name not  in self.listeners: 
            self.listeners[event_name] = [] 
        self.listeners[event_name].append(listener) 

    def  dispatch ( self, event_name, event_data ): 
        if event_name in self.listeners: 
            for listener in self.listeners[event_name]: 
                listener(event_data) 

设置
dispatcher = EventDispatcher() 
dispatcher.register_listener(
"UserPostedPhoto" , cache_listener) 
dispatcher.register_listener(
"UserPostedPhoto" , notification_listener) 

user_service = UserService(dispatcher) 
user_service.post_photo(
"123" , "beach_photo.jpg" )

实施方案和示例
上面讨论的策略为平衡 DDD 三难困境提供了一个框架。以下是一些实际示例和解决方案,可进一步说明这些概念:

变体 1:微服务架构
在微服务架构中,可以通过设计每个服务时重点关注特定领域来解决 DDD 三难问题,从而实现封装、纯度和性能的精细管理。每个微服务都可以拥有自己的领域模型,独立优化性能,同时保持封装和纯度。

示例:电子商务系统
电子商务平台可以有单独的微服务来处理订单、付款和库存。每项服务都封装了其域逻辑,通过 API 进行交互以保持纯度。可以对每项服务单独应用性能优化。

解释:

  • 订单服务、支付服务、库存服务:分别表示为节点A、B、C。它们通过API相互通信,保持了微服务架构的原则。
  • 子图:每个服务都封装在一个子图中,以指示其独立的域逻辑。这在视觉上突出了关注点的分离和每个服务逻辑的封装。
  • 双向箭头:<-->服务之间的箭头表示通过定义良好的API进行相互通信和交互,保证纯度和封装性。
  • 注释:每个服务在其子图中都有注释,描述其特定的领域逻辑和职责,反映了如何管理 DDD 三难困境。

变体 2:模块化整体式
模块化单体可以通过将应用程序划分为不同的模块(每个模块负责特定领域)来解决 DDD 三难问题。这些模块保持了封装性和纯粹性,同时利用共享基础架构来提高性能。

例如:财务应用
在财务应用程序中,可以创建用于帐户管理、交易和报告的模块。每个模块都封装了其逻辑,不受外部关注,并共享一个公共数据库以优化性能。

解释:

  • 账户模块、交易模块和报告模块:分别表示为节点 A、B 和 C。它们通过接口与公共数据库(节点 D)交互并相互交互。
  • 通用数据库:共享数据库是节点 D,所有模块都连接在此以优化性能。
  • 箭头和接口:箭头表示模块之间的交互和接口,确保逻辑的封装和纯粹。

结论
领域模型封装、领域模型纯度和性能的 DDD 三难困境对软件开发人员来说是一个巨大的挑战。然而,通过了解每个组件的复杂性并采用防腐层、CQRS、领域事件、微服务和模块化单体等策略,开发人员可以有效地解决这一三难困境。

要在这些相互竞争的目标之间取得平衡,需要深入了解领域、应用程序的需求以及每个决策中固有的权衡。通过应用本文讨论的策略,开发人员可以创建可靠、可维护且性能卓越的系统,以忠实地代表复杂的领域。