Fork me on GitHub

IDDD 实现领域驱动设计-由贫血导致的失忆症

啰嗦几句

年前的时候,在和 netfocus 兄,以及对 DDD 感兴趣园友的探讨过程中,我发现自己有很多不足的地方,对 DDD 的了解也只是皮毛而已,代码写的少,DDD 的基本概念也不是很清楚,空有一腔热爱之情是做不了事的,后来我就多写技术代码,也记录了很多的技术问题,这让我收获很多,.NET 开源等等一系列的事件,也让我们 .NET 技术阵营看到了一丝希望。

后来,在探讨的过程中,有很多我不知道的概念被讨论,比如 CQRS、六边形架构、事件溯源等等,我对这些概念是一窍不通的,像六边形架构,我只知道六边形有六个边(莫笑),这让我意识到,你只了解经典 DDD 架构,会让你自己陷入一些困境,有时候不是你自己的设计问题,而是你的眼界被遮掩住了,你需要去探寻自己视野之外的东西,这样才会有所进步。

其实,学习 DDD 最好的方式,就是用最真实的实际案例去运用,在运用的过程中,去发现问题并进行探讨学习,这样虽然会很艰辛,但收获也是巨大的,除此之外,你还会发现另一个问题,就像在建高楼大厦的时候,虽然楼房的设计是世界最高水平,但是地基打不稳,空有一张设计图纸又有什么用呢?

读《实现领域驱动设计》这本书,其实在很早的时候就计划好了,之前也读了两三章,大概是写《三个问题思考实体和值对象》这篇博文的时候,读了下实体、值对象和仓储章节,因为是带着问题读的,所以并没有很深入,只是想可以尽快从书中找到自己的答案。

昨天晚上,我大概读了第一章《DDD 入门》的前半部分,有很多内容我觉得还是蛮有意思的,我希望可以把这些东西记录下来,以防备自己的“健忘症”。

由贫血导致的失忆症

先来看书中提到的两个病例测试:

  1. 你的领域对象中是不是主要是些共有的 getter 和 setter 方法,并且几乎没有业务逻辑,或者完全没有业务逻辑-对象嘛,主要就是用来容纳属性值的?
  2. 软件组件经常使用的领域对象是否包含了系统主要的业务逻辑,并且多数情况下你需要调用那些 getter 和 setter?你可能会将这样的客户代码称为服务层(Service Layer)或者应用层(Application Layer)代码,也或者,如果这描述的是你的用户界面,请回答“Yes”,然后好好反省一下,告诫自己一定不要再这么做了。

第一个问题是领域对象的定义,第二个问题是领域对象的调用,你的回答是什么?一个 Yes、一个 No?如果你是这样的回答,作者给你这样的分析:你可能是在自欺或者患上了由贫血症导致的神经系统紊乱。哈哈,作者还蛮调皮的,回归正题,考虑这两个问题的时候,你可以和你正在做的项目进行对比考虑,是不是对你产生了一些共鸣呢?有人可能会说:唉呀妈呀,这不是我“万能”三层架构里面的 Model 层和 BLL 层嘛?如果你这么想的话,对你的最终确认结果是:先生,你患上了贫血症,而且还“贫”的不轻呢。

上面是从富有行为对象到贫血对象的时间线,凡事都有存在的理由,像贫血对象也是,它也是由多种因素导致并演化而来的,在作者叙述的这一部分内容中,我觉得主要概括为两个因素:Microsoft Visual Basic 开发方式和早期 ORM 暴露共有属性,ORM 暴露共有属性这个我不是很懂,但是 Microsoft Visual Basic 开发方式对我还是蛮有影响的,记得在上大学的时候,老师讲 Web Forms 和 Windows Forms 的课程,都是一拖一个控件,然后再设置控件的属性,这样一个项目基本就完成了,从那时候开始,“属性”的概念就慢慢培养起来了,做一个项目之前,会先把一系列的 Model 属性设计好,按照需求下面就是对这些属性值的修改,最后就是把这些 Model 保存的数据库中,过程就是这么个过程,有错吗?没有,但是呢,好像建设一栋摩天大楼的设计不应该这么简单吧?我们看下面的代码(PDF 文件,不能复制,只能纯手打):

public void saveCustomer(
    String customerId,
    String customerFirstName, String customerLastName,
    String streetAddress1, String streetAddress2,
    String city, String stateOrProvince,
    String postalCode, String country,
    String homePhone, String mobilePhone,
    String primaryEmailAddress, String secondaryEmailAddress) {

    Customer customer = customerDao.readCustomer(customerId);

    if (customer == null) {
        customer = new Customer();
        customer.setCustomerId(customerId);
    }

    customer.setCustomerFirstName(customerFirstName);
    customer.setCustomerLastName(customerLastName);
    customer.setStreetAddress1(streetAddress1);
    customer.setStreetAddress2(streetAddress2);
    customer.setCity(city);
    customer.setStateOrProvince(stateOrProvince);
    customer.setPostalCode(postalCode);
    customer.setCountry(country);
    customer.setHomePhone(homePhone);
    customer.setMobilePhone(mobilePhone);
    customer.setPrimaryEmailAddress(primaryEmailAddress);
    customer.setSecondaryEmailAddress (secondaryEmailAddress);

    customerDao.saveCustomer(customer);
}

当时,看到这个 saveCustomer 方法中的代码,我哈哈大笑了三声,笑的不是别人,而是我自己,因为我之前写过比这个 saveCustomer 方法还多的代码,那个看起来更加臃肿,之前开发的是快递业务系统,一个表多的话有近上百个字段,那修改这个表的属性,就是像上面的代码一样,不同的是,我的比这个更多,一坨一坨的。比如上面,不管是地址变了没变,你都是使用的 saveCustomer,那这个方法到底是什么含义呢?你也说不清楚,因为它看上去是那么的“万能”,不过,也确实如此。因为你说不清一个方法的具体作用,这样导致的结果就是失忆症,原因是由贫血模型产生。

举个例子,有一天,业务人员告诉 DBA(业务实际掌握人),要去掉 Customer 中的一个属性,然后 DBA 就在 Customer 表中,把这个属性对应的字段去掉了,但是 DBA 并没有告知你,因为他觉得没必要(你又不懂业务),但是,你发现项目突然报错了,然后你就各种排查,最后发现是 saveCustomer 方法里面抛出的异常,然后你就开始一个一个比较 Customer 模型属性和 saveCustomer 表字段,发现原来是少了一个字段,然后,你就和 DBA 干了起来。。。

以上纯属虚构,如有雷同,那就雷同吧,针对 saveCustomer 出现的问题,作者简要总结了下:

  1. saveCustomer() 业务意图不明确。
  2. 方法的实现本身增加了潜在的复杂性。
  3. Customer 领域对象根本就不是对象,而只一个数据持有器(data holder)。

以上的三大问题,就是导致“失忆症”发生的根本原因。

Ubiquitous Language-通用语言

在领域驱动设计中,通用语言是非常重要的一个概念,在书中的第一章节中,作者也反复提到这个概念,并进行了详细解释,我之前认为通用语言就是代码,领域专家和开发人员都可以看懂的代码,但这种理解是片面的,领域专家是业务专家,他又不是开发人员,怎么能看懂代码呢?来看几个对话:

  1. 很明显,通用语言是一种业务语言。
    抱歉,不是。
  2. 通用语言必须采用工业标准术语。
    不完全是。.
  3. 通用语言是领域专家专用的。
    对不起,不是。
  4. 通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员。
    对了。

在理解通用语言之间,还有个问题也容易混淆,至少我是这样的,那就是设计和实现的区别,有人就说了:很简单啊,这有什么好混淆的,设计就是我们画的业务流程图或者是 UML,实现就是代码。仔细一想,好像也确实是这样,但是在领域驱动设计中,领域模型的设计是通过与领域专家进行讨论确定的,画的各种设计图,并不是领域驱动的设计,而只是我们建设讨论的一种方式而已,那设计是什么?设计其实就是代码,代码就是设计,所以,在领域模型的设计中,不要把设计和实现的概念区分开,他们其实是一个概念而已。

在上面对话的第四点中,通用语言是团队自己创建的公用语言,什么意思呢?公用的意思,就是所表达的内容领域专家和开发人员都懂,语言其实不是说话的语言,中文?英文?都不是,也不是代码语言,它其实是沟通的一种方式,大家都可以理解的一种方式,一个团队有一个属于自己的公用语言,范围是仅限于团队内容,可能这个公用语言在其他团队就不适用了,也就说,它是团队成员自己创建的,当然也不是一下就可以创建出来的,是一步一步进行完善,需要每一个领域专家和开发人员的参与。

不管怎么理解,公用语言概念中,有一点是非常重要的,那就是沟通,可能说多了不好理解,作者就举了一个示例:

你会发现,业务描述的不同,最后实现的代码就会千差万别,也就是说开发人员和领域专家的沟通很重要,当然开发人员的理解能力也很重要,很多的方方面面,就组成了通用语言的概念。

如果你不知道怎么理解通用语言,你可以尝试用一个最小业务用例去实现并理解,比如,修改客户名称业务用例,你可以先把这个业务用例中所涉及的概念抽离出来,比如客户、客户名称,修改客户名称,需要首先找到这个具体的客户,当然,可能会有一些限制操作,但不管怎样,“修改客户名称”这个业务所表达的结果,就是这个客户的名称要被修改,所以你要实现修改客户名称这个操作,可能实现的代码就是下面这样:

public void changeCustomerPersonalName(
    String customerId,
    String customerName) {

    Customer customer = customerRepository.customerOfId(customerId);

    if (customer == null) {
        throw new IllegalStateException("Customer does not exist.");
    }

    customer.changePersonalName(customerName);
}

上面的实现代码和之前的 saveCustomer 代码,很明显的区别,首先,你修改客户名称,如果你使用的是 saveCustomer,你需要在方法参数中,传递一大堆的 null,而且整个方法内部充满了一些没必要的操作,而且你把 saveCustomer 方法描述给领域专家听,我想他们肯定也会不知所云,相反,changeCustomerPersonalName 就是通用语言的一种表达方式。

一个业务用例,从一开始的讨论,到最后的实现,整个过程中所涉及的方方面面,其实都可以理解为通用语言的表现,关于通用语言的界定问题,作者还提到几点:

  1. 通用语言在团队范围内使用,并且只表达一个单一的领域模型。
  2. 只有当团队工作在一个独立的限界上下文中时,通用语言才是“通用”的。
  3. “通用语言”并不表示全企业、全公司或者全球性的万能的领域语言。
  4. 每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方。
  5. 。。。

以上几点警示你,通用语言有一定的界定,并不是所有团队,也不是一个项目,而是一个单一的领域模型或者一个独立的界定上下文,你可以把它理解为一个领域模型或者一个独立界定上下文的具体表现,或者称之为过程体现。


以上只是简单的概念整理,并没有一些实际意义,具体的体会只能在实践中更加深刻,就记录到这!

posted @ 2015-03-10 14:43  田园里的蟋蟀  阅读(5401)  评论(12编辑  收藏  举报