DDD值对象:被遗忘的价值 – SoftwareMill Tech Blog


让我们看一看为什么将值对象方法应用于我们的代码是真的很有用哦。

我相信我们中的很多人都听说过域驱动设计(DDD),无处不在的语言以及所有这些奇特的东西。然而,我看到许多代码并不使用于基于这种方法的想法。为什么会这样?

也许它太复杂了,不能同时适合这一切?应用DDD需要付出巨大的努力和慎重的选择,这是很多领域知识和变化应用于我们的代码以及我们开发人员过去常常思考的方式。

人们更习惯于在我们的代码中使用技术级别(如字符串,整数,Map,List,循环等)术语,而不是使用业务术语或甚至点查看问题观点。

那么为什么不逐步地部分地使用DDD呢?这可能是一个很好的起点。从我的观点来看,其中一个起点是值对象(VO)。令人惊讶的是,我们经常使用原始值作为对象,方法或函数的输入值。VO怎么了?我们忘记了这个概念吗?

值对象
值对象是一个包含一些数据的不可变类的实例。重要的是 :这样的对象是由其属性的值定义的,没有标识身份(如实体)。都是一些彼此“公正平等”的数据。(banq注:实体ID标识高于其他属性数据)

我不会假装我会在这个概念上取得任何突破,也不会向你展示任何新的东西。如果您想要了解VO概念的更详细描述,请查看Martin Fowler不久前写的内容。即便如此,如果你熟悉它,那么值得重新阅读这篇优秀的帖子,以了解VO的细微差别。

玩代码
让我们考虑两个例子,说明VO被“遗忘”的价值。我们将从使用原始最初版本开始。接下来,我们将应用值对象方法,并查看代码如何更改。

虽然我将在代码示例中使用Java,但在Lombok功能的支持下,整体构思也可以在其他语言中使用。所以,继续阅读;)

什么是你的身份标识?
第一个示例如下:假设我们有一个服务接受访问令牌,我们需要从中提取业务标识符值以进行进一步处理。当前示例中,令牌是String类型。
让我们从代码的初始版本开始。当然,我们正在使用OOP并使用专用的提取器来查找令牌的值。这里是:

class BusinessIdExtractor {
   String extract(String accessToken) {
     return JWT.decode(accessToken).getClaim("businessId");
   }
}

很简单吧?几行易于阅读的代码。那么,这个版本有什么问题?我可以发现一些问题:
- 返回的值可以是任何值; 它是一些字符串,
且提供的值也可以是任何东西,
且代码不能很好地记录自己。

我想到的第一件事是包装返回的值,因此使用此类的编码器确切地知道返回的数据是什么。因此,我们引入了BusinessId类:

@Value
@Accessors(fluent = true)
class BusinessId {
    private final String value;
}

class BusinessIdExtractor {
    BusinessId extract(String accessToken) {
        return new BusinessId(JWT.decode(accessToken).getClaim("businessId"));
    }
}

当我们现在看代码时,我们可以看到,我们不再处理原始String,但我们已经命名了返回值。代码记录了自己。从该方法返回的值不是任何字符串; 我们期待的是一个商业标识符类型:BusinessId类。

输入数据怎么样?它也是一个字符串值。也许我们也应该在这里实施VO。我们来试试吧。我们创建AccessToken类并应用于该方法:

@Value
@Accessors(fluent = true)
class AccessToken {
    private final String value;
}

class BusinessIdExtractor {
    BusinessId extract(AccessToken accessToken) {
        return new BusinessId(JWT.decode(accessToken.value()).getClaim("businessId"));
    }
}

现在知我们已经道如何应该提供和期望的输入数据。但是,仍有改进的余地。既然我们遵循OOP原则,为什么不将提取机制移到AccessToken类中呢?这值得么?让我们来看看。

@Value
@Getter(NONE)
class AccessToken {
    private final String value;

    public BusinessId businessId() {
        return JWT.decode(value).getClaim("businessId");
    }
}

现在,这很方便!更改导致代码更少 - 我们不需要单独的类提取业务标识符值。我们唯一需要的是在令牌实例上调用一个方法。我们在类中封装了行为以及数据。之前使用过两种而不是三种。下一步可能是在构造函数中解码一个令牌值,所以我们不是每次都要求它businessId。

第二个案例:发送电子邮件给我
我想考虑的另一个例子是使用接受电子邮件地址和消息的方法发送电子邮件通知。

我们有一个专门的类,负责我们系统中的特定功能。我们称之为EmailSender:

public class EmailSender {
    public boolean send(String email, String body) {
        return doSend(email, body);
    }
}

很简单吧?但是,正如前面的例子,我们可以指出一些我们可以做得更好的地方。

由于我们对字符串进行操作,因此我们可以将任何数据传递给方法,如上例所示。接下来,由于有两个相同类型的输入参数,我们甚至可以使用输入参数的顺序上犯一个愚蠢的错误,并按以下方式调用该方法:send(body, email)。

首先,我们将引入VO输入参数:EmailAddress和Payload分别是电子邮件地址和邮件内容:

@Value
@Accessors(fluent = true)
public class EmailAddress {
    private final String value;
}

@Value
@Accessors(fluent = true)
public class Payload {
    private final String message;
}

public class EmailSender {
    public boolean send(EmailAddress emailAddress, Payload payload) {
        return doSend(emailAddress.value(), payload.message());
    }
}

这一举措解决了一些问题:我们知道了以什么顺序传递给方法 - 不再会犯愚蠢的错误!通过EmailAddress使用小验证逻辑扩展类,我们可以控制类所持有的数据。
通过引入Payload类可以更轻松地添加新字段(例如,消息的附件)。

返回值怎么样?我们也要重构它,不能用boolean,我们绝对可以做得更好:

@Value
@Accessors(fluent = true)
public class SendingResult {
    private final boolean isSend;
}

public class EmailSender {
    public SendingResult send(EmailAddress emailAddress, Payload payload) {
        return new SendingResult(doSend(emailAddress, payload.message));
    }
}

对于SendingResult类,我们可以对结果进行更复杂的处理,例如,我们可以提供有关发生错误的更多详细信息,或者为成功发送操作的持续时间添加更多花哨的数据(如果在域中有意义的话)。

没有什么能阻止我们将两个参数包装到单个值对象中,所以我们也这样做:

@Value
@Accessors(fluent = true)
public class Payload {
    private final String body;
}

@Value
@Accessors(fluent = true)
public class EmailMessage {
    private final Email email;
    private final Payload payload;
}

public class EmailSender {
    public SendingResult send(EmailMessage message) {
        return new SendingResult(doSend(message.email(), message.payload().body()));
    }
}

这是打开一些可能性:使用不同的实现技术作为输入参数:如将通知发送给外界的输入参数的另一个版本可能是KafkaSender或JmsSender。

总结
问题是 - 它是否值得?值对象方法添加了一些额外的代码来维护。然而,使用VO简化代码,更容易理解代码的意图是很棒的。此外,它还极大地改善了文档方面的代码。

通过使用VO的正确名称(命名很难,对吗?),我们正在记录代码。只需要一瞥就可以看到对服务/组件/方法的输入有什么期望,以及在调用系统的这一部分之后我们得到了什么。您不必考虑给定String参数所包含的数据类型。它是一些标识符吗?或者也许是一个人的地址?可能是任何文字吗?通过包装值,我们提供了更易于阅读和理解的上下文。

最后,如果我希望你从这个阅读中只有一件事,那么就这样吧 - 不要害怕(或者懒惰!)通过对其进行少量修改来使用你的代码。谁知道一个简单的改变(比如在一个原始价值的地方引入VO)将引导你,以及在移动一些代码后的下一个想法或想法。

毕竟,如果我们不自己试验,我们怎么能获得任何经验呢?不,像阅读博客或书籍,作为与会者参加会议,或观看有关互联网上最令人兴奋的主题的演示文稿等被动活动,如果您不实践所提出的想法和知识,则无关紧要。你必须付出努力并弄脏你的手。您和只有您负责推进软件开发人员的道路。但这与开发技能有关,但却很重要。