避免CRUD思维泄漏DDD领域逻辑 - mscharhag


许多软件架构试图将域逻辑与应用程序的其他部分分开。为了遵循这种做法,我们总是需要知道什么是领域逻辑,什么不是。不幸的是,这并不总是那么容易分开。如果我们做出错误的决定,领域逻辑很容易泄漏到其他组件和层中。
我们将通过查看使用六边形应用程序架构的示例来解决这个问题。
 
假设一个商店系统将新订单发布到消息系统(如 Kafka)。我们的产品负责人现在告诉我们,我们必须监听这些订单事件并将相应的订单保存在数据库中。
使用六边形体系结构,在适配器内实现与消息传递系统的集成。所以,我们从一个简单的适配器实现开始,它监听 Kafka 事件:

@AllArgsConstructor
public class KafkaAdapter {
    private final SaveOrderUseCase saveOrderUseCase;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        Order order = event.getOrder();
        saveOrderUseCase.saveOrder(order);
    }
}

的@AllArgsConstructor注释会生成一个构造函数,该构造函数接受每个字段(此处为saveOrderUseCase)作为参数。
适配器将订单的保存委托给UseCase实现。
UseCase是我们领域核心的一部分,与领域模型一起实现领域逻辑。我们的简单示例用例如下所示:

@AllArgsConstructor
public class SaveOrderUseCase {
    private final SaveOrderPort saveOrderPort;
 
    public void saveOrder(Order order) {
        saveOrderPort.saveOrder(order);
    }
}

这里没什么特别的。我们只是使用传出端口接口来保存传递的订单。
虽然所示方法可能工作正常,但我们这里有一个重大问题:我们的业务逻辑已泄漏到 Adapter 适配器实现中。

也许你想知道:什么业务逻辑?
 
我们有一个简单的业务规则要实现:每次检索到新订单时,都应该将其持久化。在我们当前的实现中,此规则由适配器实现,而我们的业务层(用例)仅提供通用保存操作。
现在假设,一段时间后,新的需求到来:每次检索到新订单时,都应将一条消息写入审计日志。
对于我们当前的实现,我们无法在SaveOrderUseCase 中写入审计日志消息。顾名思义,用例用于保存订单而不是检索新订单,因此可能被其他组件使用。因此,在此处添加审核日志消息可能会产生不良副作用。
也许解决方案很简单:我们在适配器中写入审计日志消息:

@AllArgsConstructor

public class KafkaAdapter {
 
    private final SaveOrderUseCase saveOrderUseCase;
    private final AuditLog auditLog;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        Order order = event.getOrder();
        saveOrderUseCase.saveOrder(order);
        auditLog.write("New order retrieved, id: " + order.getId());
    }
}

而现在我们让情况变得更糟。更多的业务逻辑已泄漏到适配器中。
如果auditLog对象将消息写入数据库,我们也可能搞砸了事务处理,这通常不会在传入适配器中处理。
 
使用更具体的域操作
这里的核心问题是通用的SaveOrderUseCase。我们应该提供更具体的 UseCase 实现,而不是为适配器提供通用的保存操作。
例如,我们可以创建一个接受新检索到的订单的NewOrderRetrievedUseCase:
@AllArgsConstructor
public class NewOrderRetrievedUseCase {
    private final SaveOrderPort saveOrderPort;
    private final AuditLog auditLog;
 
    @Transactional
    public void onNewOrderRetrieved(Order newOrder) {
        saveOrderPort.saveOrder(order);
        auditLog.write("New order retrieved, id: " + order.getId());
    }
}

现在这两个业务规则都在用例中实现。我们的适配器实现现在只负责映射传入数据并将其传递给用例:
@AllArgsConstructor
public class KafkaAdapter {
    private final NewOrderRetrievedUseCase newOrderRetrievedUseCase;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        NewOrder newOrder = event.toNewOrder();
        newOrderRetrievedUseCase.onNewOrderRetrieved(newOrder);
    }
}

这种变化似乎只是一个很小的差异。但是,对于未来的需求,我们现在有一个特定的位置来处理我们业务层中的传入订单。否则,随着新需求的出现,我们将更多的业务逻辑泄漏到不应该定位的地方的可能性很高。
像这样的泄漏尤其经常发生在域层中过于通用的创建、保存/更新和删除操作。因此,在实施业务操作时尽量做到非常具体。