领域驱动设计在触达系统的实践

591 阅读5分钟
知乎:zhuanlan.zhihu.com/p/78480807

作者:赵老师

前言

至少20年前,一些顶尖的软件设计人员就已经认识到领域建模和设计的重要性,形成了一种新的思潮,Eric Evans把这种思潮称为领域驱动设计(Domain-driven design)。《领域驱动设计》- Eric Evans

场景

在触达系统中最基本的使用场景是通过指定媒介(邮件、短信等)实时或定时发送信息。

常见方式

在介绍 DDD 之前,我们先介绍一下实现业务的常见方式。

  • 基于“Service + 贫血模型”的实现

这是大多数业务开发中常用的方式

class SendService {
    public boolean send(int messageId) {
        Message msg = new Message(messageId);
        if (msg.status != 1) { //状态非正常
            return false;
        }
        //所有关联的用户
        MessageUserDao mu = new MessageUserDao();
        int[] userIds = mu.findAllUserIdByMessageId(messageId);
        SmsService sms = new SmsService();
        for (int i = 0; i < userIds.length; i++) {
            //发送逻辑
            sms.send(userIds[0], msg);
        }
    }
}
  1. 所有的业务逻辑都在 Service 层实现
  2. Message 模型不包含具体业务逻辑,而是充当了数据访问对象(DAO), 也就是一个贫血模型(Anemic Model)。

这样导致的结果是本该内聚在 Message 模型中的业务分散在各个 Service 中,随着项目的持续演进,最终代码变得更加难以理解和不可维护。

  • 基于事务脚本的实现
    在上一种“Service + 贫血模型”方式中,Message 模型充当了数据访问对象(DAO)。 在不使用 ORM 的情况下,DAO 对象都没有必要存在。这样代码就退化成了事务脚本。在 Service 层直接访问持久化层(大多数情况下是直接构建 SQL 操作数据库)。
  • 基于领域对象的实现
    与上面两种主要不同的是,业务逻辑被内聚在了业务模型。

DDD实践

通用语言(Ubiquitous Language)

对于同一个业务,业务专家(一般是产品经理)使用业务术语描述具体产品逻辑。另一方面,开发人员可能会用一些描述性的、功能性的术语来理解和讨论系统,而这些术语并不具备业务专家的语言所要传达的意思。 然而任何一方的语言都不能成为公共语言,因为它们无法满足所有的需求。

就触达系统的基本场景,与业务专家沟通得到如下的通用语言

关系图

聚合(Aggregate)

根实体(ROOT ENTITY)绑定在一起的对象的集合,也称为聚合根。聚合根通过禁止外部对象保持对其成员的引用来保证在聚合内进行的更改的一致性。

如上面的关系图,消息计划是两个聚合根。

根据关系图和聚合概念得到的 UML 类图


和部分代码

//应用层
class SendService {
    public boolean send(int messageId) {
        FactoryMessage f = new FactoryMessage();
        Message msg = f.newMessage();//工厂模式实例化对象

        //使用 聚合根 来执行消息发送业务
        msg.send();
    }
}
//领域层
class Message {
    private Media media;
    private Addressee addressee;
    private Content content;

    public boolean send() {
        return this.media.send(this.content, this.addressee);
    }
}

public class Media {
    private Strategy strategy;

    public boolean send(Content content, Addressee addressee) {
        //过滤策略
        this.strategy.doOperation(addressee);
        //具体发送逻辑
    }
}

只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性

实体(Entity) VS 值对象(Value Object)

  1. 实体 一个不由自身属性定义而是由标识和它的身份定义的对象
    1. 聚合根一定是实体对象,但是并不是所有实体对象都是聚合根,同时聚合根还可以拥有其他子实体对象。
    2. 实体本身会体现出相关的业务行为,业务行为会对实体属性或状态造成影响和改变。


  1. 值对象 只包含元素属性的不可变对象
    值对象还有一个特点是不变的(Immutable),也就说一个值对象一旦被创建出来了便不能对其进行变更,如果要变更,必须重新创建一个新的值对象整体替换原有的。
思考值对象和实体区别,建议不要先有数据表设计的概念
  • 内容(Content)
    内容对于消息来说,我们只关心内容的值,如{"title":"标题","desc":"描述"},在上下文中本身没有状态,本身不具备唯一标识,脱离了消息无法独立存在
  • 媒介(Content)
    我们不关心媒介的值,而是关心具体的业务行为(发送消息)
  • 收件人(Addressee)
    对于消息而言,我们原本只是关心收件人的值,如{"mobile":"130*****","email":"****@qq.com"},但在本系统中存在更多业务属性(如:黑名单的过滤)。 黑名单不只关心值mobileemail,还可能关心收件人的唯一标识(一般为用户ID),因此设计为实体。

资源库(Repository)

对于检索特定域对象的方法应该委派给 Repository 对象,因为这样可以很容易地互换替代存储的实现。

资源库用于保存和获取聚合对象,主要用于持久化聚合对象。

public class MessageRepository {
    public boolean save(Message message){
        //写入持久化层,本系统中为数据库
    }
    public Message forMessageId(){
        //查询数据库
        //使用工厂模式创建Message聚合对象
    }
}

总结

通过 DDD 提供的技术思想,建立一种公共语言,可以在团队成员之间用来进行更加充分的沟通,并确保围绕软件来进行沟通。 在此之上创建一个与模型步调一致的清晰实现,从而为应用程序的开发提供帮助。

参考