翻出领域驱动设计的一篇旧文

文章写于10年前。
领域驱动设计(DDD)一直是一个颇具争议的话题。每个人似乎都有一些不同的理解。有的人四处吹嘘这东西好,其实他可能只是昨天晚上刚翻过Eric Evan的那本书而已。有的人敢于非议DDD,倒未必是他无知,反而往往是因为在实践中遇到过问题。看过许多公说公有理,婆说婆有理的论战。也在实践中发现了许多实际的困难。点点滴滴积累了一些自以为有点意思的看法。所以想写点东西,和大家一起来谈谈DDD这个话题。首先,我要给拥护DDD的同志讲几句,谈谈我们为什么喜欢它热爱它,顺便建立一下同志的友谊。同时,我还要替反对DDD的同志讲几句,讲讲我们为什么敢说领域模型根本不实际。到未必全是因为我这个人两面三刀,最主要的是DDD在实践中确实问题多多,要不然也不会每次现身都在江湖上引出血雨腥风。废话不多说了,爱也好,恨也罢。

爱也好

如果你觉得领域驱动开发是一个好东西,但是别人问你好在哪里,你可有答案?那你有没有想过,用DDD的理念写出来的代码和别的代码有什么不同?而你又凭什么说你写的东西是领域驱动的,别人写的就不是呢?空谈无益,先来看一段代码吧:

public class ReserveNumberService {
 
    private final static Logger LOGGER = LoggerFactory.getLogger(SelectNumberService.class);
 
    @Inject
    SqlMapClient sqlMapClient;
 
    public void reserverNumber(String userId, String number) {
        try {
            String oldNumber = (String) sqlMapClient.queryForObject("reservedNumber", userId);
            releaseOldAndReserveNew(userId, oldNumber, number);
        } catch (SQLException e) {
            LOGGER.error("failed to reserve number " + number + “ for user “ + userId, e);
        }
    }
 
    private void releaseOldAndReserveNew(String userId, String oldNumber, String newNumber) throws SQLException {
        sqlMapClient.startTransaction();
        try {
            if (oldNumber != null) {
                sqlMapClient.update("releaseNumber", oldNumber);
            }
            HashMap params = new HashMap();
            params.put("number", newNumber);
            params.put("usreId", userId);
            if (sqlMapClient.update("reserveNumber", params) == 0) {
                throw new RuntimeException(newNumber + " already reserved by someone else");
            }
            sqlMapClient.commitTransaction();
        } finally {
            sqlMapClient.endTransaction();
        }
    }
}

你的第一印象是什么?我的印象是满屏的sqlMapClient。你能第一眼就说出这段代码中出现了哪些概念,又解决了什么问题吗?或许不是那么容易的事情。如果用DDD的方式来写,也许是这样:

public class PhoneNumber {
 
    private final String number;
    private User reservedBy;
 
    public PhoneNumber(String number) {
        this.number = number;
    }
 
    public void reservedBy (User user) {
        reservedBy = user;
    }
 
    public void release() {
        reservedBy = null;
    }
}
public class User {
 
              private PhoneNumber reservedNumber;
 
              public void reserve(PhoneNumber number) {
                         if (reservedNumber != null) {
                                     reservedNumber.release();
                         }
                         number.reservedBy(this);
                         this.reservedNumber = number;
              }
}

通过阅读上面的代码,我们会发现其中牵涉到了两个领域概念,分别是User和PhoneNumber。要建模的问题其实是User如何Reserve一个PhoneNumber。业务逻辑是PhoneNumber只能被一个User预定,而且一个User只能预定一个PhoneNumber,如果要预定新的必须先释放旧的。
也许上面的那段业务分析可能就出现在selectNumber这个方法的注释里,也可能是在某某详细设计文档里。但是为什么不能让代码自身反映出这些业务上的概念呢?在我看来,让代码成为永不过期的文档就是DDD整套理论的出发点。
使用DDD,当我们写代码的时候,就可以关注在PhoneNumber和User的逻辑关系上。当我们和其他开发人员交流的时候,就可以讲PhoneNumber如何如何,User如何如何,而不用说某某表的某某字段是什么值。当我们和业务人员交流的时候,至少我们不用在对话里提到sqlMapClient如何如何。这些好处归纳出来就是两点:
分离关注点
把领域模型与其他代码中分离之后,注意力就能够更加集中。写PhoneNumber和User的时候,就不用去担心从哪个数据表中把他们取出来。去Reserve一个PhoneNumber的时候,也不用担心Release旧的电话号码与Reserve新的电话号码必须是在同一个事务中完成的。这样一来,读领域模型代码就能更集中精力于业务上是不是逻辑正确的。而在我们读事务处理的代码的时候,就能更关注于事务的边界是不是正确。
共同语言
领域驱动开发另一个最强大之处是,它利用领域模型把业务人员使用的语言规范化文档化了。领域模型中的各种Entity类给语言提供了名词。各个Entity上的方法给语言提供了动词。只要我们精心地去维护调整,开发人员就能和业务人员共享同一个词汇表。有了共同的语言,沟通的效率就会提高。

恨也罢

可能不及把上面的文字看完,有的读者就已经跳出来了。你这个骗子!我承认,我说的不全是事实。比如说上面那个例子,如果领域模型不管数据库了,不管事务了,谁来管?写领域模型的代码的轻松是以写OR Mapping代码的痛苦为代价的。这些痛苦,我也感同身受。而且OR Mapping只是问题的冰山一角,还有其他方面的各种各样的问题:
持久化问题
有时候感觉这逻辑没法用领域模型来写。有的时候,觉得这ORM怎么这么慢啊。ORM工具入门挺容易的,要用得好可难了。
分层问题
Controller委托给Service,Service委托给DAO。分层在实践中似乎并不是想象中那么强大,反而感觉非常麻烦。领域模型在分层中是什么位置?据说领域模型层负责领域逻辑,服务层负责工作流逻辑,表现层负责表现逻辑,实践中咋分得清呢?验证逻辑是放在哪层来做的?
C/S架构问题
在普通WEB应用中使用DDD已经有不少问题了,在C/S架构下会有更多的问题。客户端服务器之间需要跨网络操作,领域模型在哪?如果双方都有,如何互相通信?
代码膨胀问题
随着开发的进行,系统变得越来越复杂,领域模型由于综合了所有的业务逻辑,会变得越来越大。如果论坛的功能加到了User上,聊天的功能也加到了User上,游戏的功能也加到了User上。User岂不是越来越复杂了吗?
集成问题
企业应用的一个特点就是系统之间需要互相集成来共同完成业务。DDD似乎更加关注单一系统内部如何设计得尽善尽美,在一件事情需要多方参与的时候,似乎就不是那么容易使用DDD。

模型驱动

这些问题是DDD独有的吗?其实未必。DDD与传统的开发方式的主要不同就在于,过去我们是依赖于底层系统的API直接去让它去做去做那。而DDD则是模型驱动的,所有的逻辑都通过模型来表达,由不同的Mapper把模型映射为对底层系统API的调用。打个比方说,传统的方式就像画画时关注一笔一划怎么画的,但是并不能一眼看出画了什么。而DDD则直接表达画的最终模样,具体一笔一划怎么画的领域模型并不关心。上面这些问题之所以是问题,一方面是开发人员不习惯这种思考问题的方式,以及缺乏用模型驱动的方式去解决实际问题的经验。另外一方面是现有的工具和平台对模型驱动的开发方式支持不够好。
尝试去隔离出一个纯净的模型层的不止是DDD。其实模型驱动我们每天都会遇到,比如MVC不就是用Model驱动View吗?回忆过去,MDA宣称的不正是模型的通用,与技术平台无关吗?而展望未来,DSL不是也在做类似的事情吗?只是披了一层语言的外衣而已。在DDD之中,模型特指用面向对象建模出来能够反映业务模型的对象模型。而在MDA中,模型可能就是UML,在DSL中模型可能就是某种自己发明的语言。

结论

很多前辈在讨论领域驱动开发的时候都强调其成本。上面说的这些问题,就是成本。这些问题如果解决不好,很轻易地就能把DDD所能带来的好处都给赔掉。从DDD这个名词被发明到现在已经很久了,但是这些实际项目中的问题却仍然困扰着人们。我们来谈谈DDD,就是要分析分析这些问题。限于在下的能力以及文章的形式,不可能详尽地列举各种解决方案。最主要的目的是希望弄明白问题是什么,以及提供一些值得一试的新思路开拓一下思维。

参考资料

非常感谢Eric Evan,Greg Young,Richard Oberg,Randy Stafford。
Eric Evan: Strategic Design


http://www.
viddler.com/explore/ore
dev/videos/43/

Randy Stafford:Domain Model Persistence


http://www.
viddler.com/explore/ore
dev/videos/40/

Richard Oberg: Qi4j Persistence

http://www.qi4j.org

Greg Young: Distributed DDD


http://
codebetter.com/blogs/gr
egyoung/archive/tags/DDDD/default.aspx

Greg Young: Unshackle Your Domain


http://www.
infoq.com/presentations
/greg-young-unshackle-qcon08

Greg Young: Eric Evans Interviews Greg Young on the Architecture of a Large Transaction System


http://www.
infoq.com/interviews/Ar
chitecture-Eric-Evans-Interviews-Greg-Young

Greg Young: Greg Young Discusses State Transitions in Domain-Driven Design and DDD Best Practices


http://www.
infoq.com/interviews/gr
eg-young-ddd

Greg Young: Altopia Corporation Information

Canada DDD Chalk Talk


http://www.
viddler.com/explore/bsi
mser/videos/2/

Greg Young: DDDD DevTeach Talk


http://www.
viddler.com/explore/Gre
gYoung/videos/5/