如何使用DDD优雅地避免来自域实体的基础结构服务的依赖?

时间:2017-11-13 00:53:55

标签: oop domain-driven-design

背景

假设我的任务是使用域驱动设计(DDD)在通知发送域中构建系统。该系统的一个关键要求是它需要支持各种“类型”的通知,如短信,电子邮件等。

在开发域模型的几次迭代之后,我继续着手将Notification基类作为实体,子类SMSNotificationEmailNotification等作为子类(每个都是一个实体)。

Notification

public abstract class Notification extends Entity<UUID> {
    //...fields...

    public abstract void send();
}

SMSNotification

public class SMSNotification extends Notification {

    public void send(){
         //logic for sending the SMS notification using an infrastructure service.
    }
}

EmailNotification

public class EmailNotification extends Notification {

    public void send(){
        //logic for sending the email notification using an infrastructure service.
    }
}

问题(S)

  • 使用这种当前的设计方法,Notification的每个子类都与基础结构服务进行交互,其中基础结构的任务是与某些外部系统连接。
Eric Evans在介绍domain services的概念时,在第107页的“领域驱动设计”一书中专门介绍了一些关于此的页面空间:

  

...,在大多数开发系统中,在域对象和外部资源之间建立直接接口是很尴尬的。我们可以打造这样的外部服务,其外观采用模型方面的输入,......但无论我们拥有什么中介,即使它们不属于我们,这些服务也在履行域名责任。

  • 相反,我使用Evans的建议在我的域模型中获得SendNotificationService,而不是在send的每个子类上使用Notification方法,我不知道如何避免需要知道提供了哪种类型的通知,以便采取适当的基础架构行动:

SendNotificationService(域名服务)

public class SendNotificationService {
    public void send(Notification notification){
        //if notification is an SMS notification...
        //    utilize infrastructure services for SMS sending.
        //if notification is an email notification...
        //    utilize infrastructure services for email sending.
        //
        //(╯°□°)╯︵ ┻━┻)
    }
}

我在这里缺少什么?

  • 面向对象的设计原则正在推动我使用NotificationSMSNotificationEmailNotification类来首先建议模型。在send的每个子类上实现Notification方法是有意义的,因为需要发送所有通知(证明其在Notification中的位置)以及Notification的每个“类型”或子类将在发送通知的方式中具有特殊行为(证明send中的Notification抽象是合理的。这种方法也遵循开放/封闭原则(OCP),因为Notification类将不再修改,并且当支持新的通知类型时,可以创建Notification的新子类来扩展功能。 无论如何,似乎没有实体与外部服务接口,以及在DDD中根本没有实体的子类。
  • 如果从Notification删除了发送通知的行为,那么放置通知的位置必须知道通知的“类型”,并采取相应的行动,我只能将其概念化为{{1}的链}语句,与OCP直接矛盾。

3 个答案:

答案 0 :(得分:1)

TLDR:如果您需要针对您的域执行某些基础架构逻辑,并且您需要从域中输入一些内容 - 不要将其构建到其中,只需使用适当的数据/标记声明意图。然后,您将在基础架构层中处理此声明的意图。

各种通知是否与其他交付机制不同?如果不是 - 可能足以使用Notification值对象(或实体,如果您的域模型需要)与附加字段(Enum,如果列表已知或某种标记)来存储传递方法名称。也许,每个通知实例可能有很多这样的方法。

然后你有一个业务逻辑 - 域服务 - 来发送通知。域服务应该仅依赖于域词汇表。 E.g NotificationDeliveryMethodProvider。

在适配器层中,您可以实现各种交付方法提供程序以与基础结构进行交互。以及根据DeliveryMethod枚举(或标记)中的值获取提供者的工厂。

基本上,它并不是发送&#34;发送&#34;的责任。本身就是以任何方式操纵。它的职责应该是维持其状态,以一致的方式执行状态转换并协调其封闭实体/值的状态。关于其状态的火灾事件发生了变化。

在我的一个项目中,我在domain包下使用了以下子包:

  • provides - 提供给客户的域服务接口
  • cousumes - 上游依赖的接口
  • businesslogic - 域名服务的实施
  • values - 使用代码实现其不变量的值对象
  • ...

除了domain包之外,还有:

  • adapters打包基础架构
  • App对象,其中所有接口都绑定到实现。
  • [可能还有] config包,但在我的情况下它非常轻。

这些domainadaptersAppconfig可以部署为具有明确依赖关系结构的不同jar文件,如果您需要为其他人强制执行它。

答案 1 :(得分:0)

  

在开发域模型的几次迭代之后,我继续着陆将Notification基类作为实体,将子类SMSNotification,EmailNotification等作为子类

这可能是一个错误。

public abstract class Notification extends Entity<UUID> {
    public abstract void send();
}

几乎可以肯定的是。你可以让它运作,如果你扭曲了,但是你走错了路。

您的域模型中的实体的责任是 state 的管理。还要让实体负责在整个流程边界内调度消息的副作用,这违反了关注点的分离。所以成为合作者。

对于Evans,正如您将注意到的,协作采用域服务的形式,它本身将与基础结构服务协作以产生所需的结果。

让实体访问域服务的最直接方式是简单地将域服务作为参数传递。

public class SMSNotification extends Notification {
    public void send(SMSNotificationService sms) {
        //logic for sending the SMS notification using an infrastructure service.
    }

SMSNotification支持与SMSNoticationService提供商的协作,我们明确说明。

您在此处提供的界面看起来更像Command Pattern。如果你想做到这一点,你通常会在构造函数中连接特定的实现

public class SMSCommand extends NotificationCommand {
    private final SMSNotificationService sms;
    private final SMSNotification notification;

    public final send() {
        notification.send(sms);
    }
}

您可以使用泛型(取决于您的语言选择)做一些事情,使这些不同服务之间的相似之处更加明显。例如

public abstract class Notification<SERVICE> extends Entity<UUID> {
    public abstract void send(SERVICE service);
}

public class SMSNotification extends Notification<SMSNotificationService> {
    public void send(SMSNotificationService service){
        //logic for sending the SMS notification using an infrastructure service.
    }
}

public class NotificationCommand<SERVICE> {
    private final SERVICE service;
    private final Notification<SERVICE> notification;

    public final send() {
        notification.send(service);
    }
}

这是主要方法。

有时适合的替代方法是使用穷人的模式匹配。不是传递特定类型实体所需的特定服务,而是将它们全部传递给....

public abstract class Notification extends Entity<UUID> {
    public abstract void send(SMSNotificationService sms, EmailNotificationService email, ....);
}

然后让每个实现精确选择它需要的东西。我不希望这种模式在这里成为一个很好的选择,但它是一个偶尔有用的俱乐部。

您有时会看到的另一种方法是在构造实体时将所需的服务注入实体

SMSNotificationFactory {
    private final SMSNotificationService sms;

    SMSNotification create(...) {
        return new SMSNotification(sms, ...);
    }
}

再一次,一个好的俱乐部可以放入包中,但不适合这个用例 - 你可以做到,但突然有很多额外的组件需要知道通知服务,以便将它们带到他们需要的地方是。

  

notification.send(service)和service.send(通知)之间的最佳选择

可能

notification.send(service)

使用&#34;告诉,不要问&#34;作为理由。您将协作者传递给域实体,它决定(a)是否要进行协作,(b)传递给域服务的状态,以及(c)如何处理返回的任何状态。

SMSNotification::send(SMSNotificationService service {
    State currentState = this.getCurrentState();
    {
        Message m = computeMessageFrom(currentState);
        service.sendMessage(m);
    }
}

在边界处,申请不是object oriented;我怀疑当我们从域的核心向域移动时,我们看到实体让位于值让位于更原始的表示。

  

在阅读了纯粹的域模型之后,事实上不应该有任何IO,我不再确定

事实上,这有点纠结。域服务的一个动机是将域模型与IO分离 - 所有IO关注点都由域服务实现(或者更可能由域服务与之协作的应用程序/基础结构服务)处理。就实体而言,所涉及的方法只是一种功能。

另一种方法是在关注点之间创造更多分离;您将两部分之间的编排显式化

List<SMSRequest> messages = domainEntity.getMessages();
List<SMSResult> results = sms.send(messages)
domainEntity.onSMS(results)

在这种方法中,所有IO都发生在短信服务本身内;与模型的交互被限制在内存表示中。您已经有效地获得了一个协议,该协议可以管理模型中的更改以及边界处的副作用。

  

我觉得Evans建议将service.send(通知)作为界面。

我认为,课程的马匹 - 将实体传递给负责协调模型中多个变更的域服务是有道理的。在更改为聚合的上下文中,我不会选择用于将状态与边界进行通信的模式。

答案 2 :(得分:0)

我同意你的看法Notification的主要责任应该是,它可以发送自己。这就是它存在的全部原因,因此它是一个很好的抽象。

public interface Notification {
    void send();
}

此接口的实现您正在寻找的基础结构服务。他们不会(不应该)直接被其他&#34;业务&#34;或&#34;核心&#34;类。

关于在Entity中制作的注意事项:我自己阅读蓝皮书的内容是,DDD 关于使用实体,服务,聚合根等等。主要观点是无所不在的语言,上下文,如何使用域本身。 Eric Evans自己说这种想法可以应用于不同的范例。它不必总是涉及相同的技术事物。

注意&#34;传统&#34;设计来自其他评论(@VoiceOfUnreason):至少在面向对象中,&#34;持有状态&#34;不是一个真正的责任。责任只能直接来自无所不在的语言,换言之,来自企业。 &#34;传统的&#34; (即程序)设计分离数据和功能,面向对象完全相反。因此,请务必确定您的目标范围,然后选择解决方案可能更容易。