设计:当领域对象和服务对象之间的界限不清晰时

7
这个问题本质上是一个设计问题。我将使用一个Java/Java EE的例子来阐述这个问题。
考虑一个使用JPA持久化和EJB服务层构建的Web邮件应用程序。假设我们在EJB中有一个像这样的服务方法:
public void incomingMail(String destination, Message message) {
    Mailbox mb = findMailBox(destination); // who cares how this works
    mb.addMessage(message);
}

这似乎是一种合理的业务方法。可以想象,邮箱对象仍将保持连接,并无缝地将更改保存回数据库。毕竟,这就是透明持久性的承诺。
邮箱对象将拥有此方法:
public void addMessage(Message message) {
    messages.add(message);
}

这就是变得复杂的地方——假设我们想要其他类型的邮箱。比如,我们有一个自动回复邮件箱,可以自动回复给发送者,还有一个HelpDeskMailbox,它可以在收到每封邮件时自动打开一个帮助台工单。

最自然的做法将是扩展Mailbox类,其中AutoRespondingMailbox具有以下方法:

public void addMessage(Message message) {
    String response = getAutoResponse();
    // do something magic here to send the response automatically
}

问题在于我们的"邮箱 Mailbox"对象和其子类是"领域对象"(在本例中也是JPA实体)。Hibernate团队(以及许多其他人)倡导一种非依赖性的领域模型——即不依赖于容器/运行时提供的服务的领域模型。这种模型的问题在于AutoRespndingMailbox.addMessage()方法无法发送电子邮件,因为它无法访问JavaMail等服务。
对于HelpDeskMailbox来说,完全相同的问题会发生,因为它无法访问WebServices或JNDI注入以与HelpDesk系统通信。
因此,您被迫将此功能放在服务层中,如下所示:
public void incomingMail(String destination, Message message) {
    Mailbox mb = findMailBox(destination); // who cares how this works
    if (mb instanceof AutoRespondingMailbox) {
        String response = ((AutoRespondingMailbox)mb).getAutoResponse();
        // now we can access the container services to send the mail
    } else if (mb instanceof HelpDeskMailbox) {
        // ...
    } else {
        mb.addMessage(message);
    }
}

在这种情况下使用instanceof是问题的第一个迹象。每次想要子类化Mailbox时都需要修改此服务类,这是另一个问题的迹象。
有没有最佳实践来处理这些情况?有些人会说Mailbox对象应该可以访问容器服务,并且可以通过一些调整来实现,但这明显是与JPA的预期用法相抵触的,因为容器在除实体外的所有地方都提供了依赖注入,清楚地表明这不是预期的用例。
那么,我们应该怎么做呢?难道我们要放弃多态性吗?我们的对象自动变成C风格的结构体,失去了大部分面向对象的好处。
Hibernate团队会说,我们应该将业务逻辑分为领域层和服务层,在领域实体中放置所有不依赖于容器的逻辑,并将所有依赖于容器的逻辑放入服务层。如果有人能给我一个不必完全放弃多态性并诉诸于instanceof等恶劣方法的示例,我可以接受这一点。
4个回答

6
您缺少了一些内容:Mailbox对象完全可以依赖于在运行时提供的接口,这是完全合理的。 "不要依赖于运行时服务"是正确的,因为您不应该具有编译时依赖关系。
只有一个接口依赖时,您可以使用像StructureMapUnity等IoC容器来向对象提供测试实例而不是运行时实例。
最终,您的AutoRespondingMailbox代码可能如下所示:
public class AutoRespondingMailbox {
    private IEmailSender _sender;

    public AutoRespondingMailbox(IEmailSender sender){
        _sender = sender;
    }

    public void addMessage(Message message){
        String response = getAutoResponse();
        _sender.Send(response);
}

请注意,这个类确实依赖于某些东西,但不一定由运行时提供 - 对于单元测试,您可以轻松提供一个写入控制台的虚拟 IEmailSender 等。此外,如果您的平台发生变化或需求发生变化,您可以在构造时轻松提供一个不同的 IEmailSender,其使用的方法与原始方法不同。 这就是“限制依赖关系”态度的原因。

我认为Hibernate的开发人员会持不同意见。他们真的相信业务逻辑可以/应该被拆分。但我不明白如何在不把整个系统搞乱的情况下进行拆分。 - TTar
这不是在分离业务逻辑吗?AutoRespondingMailbox中唯一的逻辑是它应该从某个地方获取响应,并自动发送它。它不依赖于除特定接口存在之外的任何内容。该接口可以通过许多方式提供,其中许多与已编译的代码根本没有关系。 - Harper Shelby

4

一个邮箱就是一个邮箱...

...但自动回复的邮箱则是带有一些规则的邮箱;可以说这不是邮箱的子类,而是控制一个或多个邮箱和一组规则的MailAgent。

注意:我在DDD方面经验有限,但我认为这个例子基于一个错误的假设,即应用规则的行为属于邮箱。我认为对消息应用规则与邮箱无关,即收件人邮箱可能只是过滤/路由规则使用的标准之一。因此在这种情况下,一个ApplyRules(message)或ApplyRules(mailbox, message)服务更加合理。


我认为你错过了我问题的意图。忘记例子的具体细节,假设一个实体的某些子类需要依赖于一些由容器提供的服务来实现其行为。 - TTar
那么,如果一个域对象只是一个简单的数据持有者,那我的域对象不就是一个结构体吗?难道面向对象设计的定义不是将数据和行为组合成对象吗? - TTar
1
@[TTar]:抱歉,我无法满足您的要求,因为它与所给示例的语义不符。换句话说,我无法解决您问题中假设的设计问题(可以说是设计上的错误)。在您的示例中,更合理的做法是创建一个ApplyRules(mailbox, message)服务来保持一切分离。 - Steven A. Lowe
@Steven -- 是的。在这种情况下,子类化Mailbox以添加规则似乎违反了SRP原则。 - Dave Markle
@[TTar]:嗯...不行。因为自动响应行为在逻辑上并不属于邮箱[邮箱只是一个命名的容器,仅此而已]。但如果你喜欢,可以随意实现这种方式,它可能仍然有效,并且未来可能永远不会给你带来任何问题。不过我不会那样做。 - Steven A. Lowe
显示剩余2条评论

1

我对DDD没有太多经验,但我有一个建议来解决这个问题。

我会将MailBox类声明为抽象类,然后将3个实现类作为MailBox的子类。

我认为addMessage(...)方法的命名可以更好地完成。这个名称 - add表明所提供的消息应该只是添加到邮箱中,就像一个setter一样,但是不会替换现有值,而是将所提供的消息添加到某种存储中。

但你要寻找的是一种行为。如果抽象邮箱强制所有子类实现方法public void handleIncommingMessage(Message message); 該怎么辦呢?

然后您的方法findMailBox(destination)以某种方式决定应该检索哪个邮箱实例,这已经是它的责任了。

在实例化邮箱的不同子类时,每个子类可能具有处理传入消息的不同需求。但是可以通过以下方法进行分离:

功能接口:

public interface MessageHandler {
    void handleMessage(Message message);
}

抽象类:

public abstract MailBox{
    private MessageHandler handler;

    protected MailBox(MessageHandler handler){
        this.handler = handler;
    }

实例化:

 MailBox mb1 = new MailStorage(new DefaultMessageHandler());
 MailBox mb2 = new AutoreplyingMailBox(new AutoReplyingMessageHandler()); 
 MailBox mb3 = new HelpDeskMailBox(new HelpDeskMessageHandler());

如果您希望的话,甚至可以摆脱所有不同的MailBox子类,而是只制作MessageHandler接口的不同实现。
根据提供给findMailBox方法的目标,您只需要实例化一个MailBox(在这种情况下为非抽象),并向其提供正确的MessageHandler实现。
这将使MailBox.handleIncommingMessage(...)只做一件事情(或两件事情)。
public class MailBox {

    private MessageHandler messageHandler;

    public MailBox(MessageHandler messageHandler){
         this.messageHandler = messageHandler;
    }

    public void handleIncommingMessage(Message message){
         addMessage(message);
         this.messageHandler.handleMessage(message);
    }
}

您的示例中的最终代码将如下所示:
public void incomingMail(String destination, Message message) {
    Mailbox mb = findMailBox(destination); // who cares how this works
    mb.handleIncommingMessage(message);
}

这种方法在引入新的MailBox或MessageHandler类型时,永远不必编辑。逻辑与数据分离,添加消息时发生的操作(addMessage/handleIncommingMessage)的逻辑保存在MailHandler实现中。

0

一种选择(也许不是最好的选择)是将对象包装在“执行器”对象中。执行器对象将包含服务层信息,而内部化的数据对象将包含域信息。然后,您可以使用工厂来创建这些对象,从而限制“instanceof”方法或类似元素的范围,然后不同的对象将具有某种常见接口可用于执行其数据对象。它有点像命令模式 - 您将命令对象作为执行器 - 和状态模式 - 状态是数据对象的当前状态 - 尽管两者都不是完全匹配。


但我们这样做的目的是为了将容器依赖性与我们的领域模型分离。这样做有何意义呢?只是为了让事情变得更困难吗? - TTar
允许不同的前端服务使用相同的后端系统,这是我的理解。 - aperkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接