领域驱动设计之如何编写类?

板桥banq 原创书籍《复杂软件设计之道:领域驱动设计全面解析与实战》

目录

  1. 领域的定义
  2. 如何编写类?
  3. 对象如何创建
  4. SQL语句要不要写

第二章

  上一章我们讨论了领域与有界上下文,并以电力结算系统为案例找到了位于领域两个边界点上的上下文: 交易中心购电结算和营销部售电结算。

  我们还可能处于盲人摸象的过程中,但是谁不是呢?谁也不是上帝能一下子透彻看到本质,所谓抓住本质只是自大狂们的自以为是而已。

  这里瞎掰一小段哲学,不愿意烧脑可跳过这段啊,面向对象分析设计中有一个对象,还有一个词: 现象,这两个词都有'象',但是'对象'和'现象'却代表截然相反的两个世界观,对象派认为世界肯定存在本质,那个本质就是存在于我们意识之外的物质对象,这也就是所谓本体论,但是现象派认为世界都是现象,没有本质对象,你以为的本质对象其实还是现象,世界如同洋葱,你剥开一层洋葱皮后还是洋葱皮,你发现一层现象后面还是现象,而且这种现象是带有人的角度和意识的,这里扯了点胡塞尔现象学的皮毛。

  回到本文主题,我们对电力结算系统认识可能还是片面,但是也许就没有本质整体,当然洋葱还是要一层层剥下去的,我们在代码阶段可以不断精炼领域知识。

包名

我们将购电结算与售电结算两个上下文先用模块固定下来,模块在代码层面怎么实现呢?用包啊,包package这个这个名字多好,打包隔离,边界意识很强烈哦。购电上下文用com.jdon.ddd.two.purchase; 电费结算用con.jdon.ddd.two.order。

在具体取包名时,争取使用上下文名称,名称本身也是界定了上下文边界,最好用英文,当成专业英语学习了,purchase是采购的意思,但是没有明确采购的是什么?在目前默认语境中,应该知道是采购电力,随着系统复杂,可能以后需要进一步重构。

取名
----------------------------
现在我们的包有了,下面应该是类名,平时我们写代码时,是不是顺序有点颠倒,总是在创建类时随带把包名填写上,那么对包或模块概念可能就缺乏仔细想一想的过程。
类应该对应于有界上下文里面的领域模型名称,前面我们只设计了上下文,领域模型没有确定,只是大概知道体现领域知识和逻辑概念,那么现在就要确定在这两个上下文中体现逻辑的概念是什么?

名可名非常名,取名不是拍脑袋,不是用联异想天开,取名需要代表逻辑概念的。

在购电这个模块中,我们看看功能实现要求是什么,首先购电方需要和我们签订合同,购电方需要查询其电量使用情况。这里有一个逻辑顺序: 首先需要签合同,然后才有电量使用跟踪,为什么称为逻辑顺序呢?,因为前后步骤不能乱,你没有进货怎么卖货呢?这里包含一种逻辑一致性约束,进货是卖货的必要条件和前提,只有进货了才能卖货。可能有人到现在还觉得电力结算这个案例不太熟悉,我们洋葱已经剥到逻辑这一层: 它就是进货卖货系统,进销存系统,或是淘宝系统,只不过这货是'电'。

好吧,有人说,既然进货是卖货先决条件,那么我们先从合同下手,我们就建立一个合同的数据表吧,然后实现合同的增删改查,瞧,这个微服务或就很快完成了,但是这么做真的是好吗?我们不谈对不对,因为没有人可以评判别人的做法,但是如果以工匠要求,恐怕这样做没有抓住重点,虽然合同是先决条件,但不是主要的,合同只能代表一个逻辑流程的起点,而我们完成的目标应该是购电这个上下文边界內的逻辑概念模型。

发现主要实体
--------------------------------
这里打个比喻,我们看到一条河,水在其中流,水流是一种逻辑流程,那是什么主体在流动呢?很显然是'水'这个主体。

那我们这里的逻辑流主体是啥?有时候我们如果发现不了主体,或者主体比较多,那么可能意味着上下文边界太粗粒度,假设我们现在的上下文划分没有问题,这里主体有的童鞋说还是合同,合同签订到运行监督形成了一个合同的完整生命周期,在这个逻辑流里面我们看到了合同这个主体吗?是合同在流动吗?

如果我们开发的是一个通用合同管理系统,专门给公司办公室管理使用,也许合同确实是主体,但是我们的领域是电力结算系统,合同只是手段,不是目的,那么目的是什么?

这也许对于不同用户方是不同的,对于供电企业,他们的目的是把电力卖得越高使用越多,电量和电价是他们关心的关键目的。

购电企业购电目的也是电量和电价,只不过需要同时考虑到下游用电单位的目的,这里不多说,因为购电企业和供电企业目的逻辑是一致的,他们才会签订合同,因此,这里的主体应该是体现大家共同的目的,因为这个目的才签订合同,兜了这么多圈子,双方签订的目的是啥?约定好电价啊,因此,在合同阶段电价是主体。

签订好合同后,供电企业虽然关心电量,但是最终目的还是关注电价乘以电量的结果,这个结果是啥?这时我们可能又剥洋葱剥了一层,把隐含在的概念显现化了。

签合同是确定电价,当然用电量对电价也有影响,超过多少用电量,用电单价可以下调几个百分点,用电时间段对电价也有影响,晚上十一点以后用电单价是对折等,甚至和发电机组有关。这些就属于逻辑规则了,这些都需要在合同里面约定清楚,然后在后面用电过程中根据这些用电规则进行实际购电费结算。

新一层洋葱皮终于露出水面,购电费应该是购电上下文的核心概念。

以上是一种思考路径,条条大路通罗马,下面我们再看看另外一种思考路径。

购电上下文这个模块中,签订合同是为了确定电价,后续关注用电量跟踪情况,如果认为用电量是这个阶段的主体,因为我们需要知道精确时刻刻度內的用电多少。

现在这里有三个基本主体变量: 电量 电价和用电时间段,这三者不是独立的,相互影响,只有组合在一起,加上逻辑规则,才能组合成一个真正模型,这个组合起来模型是什么?我们需要用一个名词或动名词去一言以蔽之,这就是类名的来由。

电价+电量+用电时间段+用电规则 = 购电费

为了慎重起见,领域专家特地查查英文词汇,因为如果它是一个重要模型词语,就不应该是新词汇,应该是人们常用很久的一个词汇,果然,领域专家找到了electricity rates这个词语,而electricity charge适合售售上下文环节的模型名。

领域模型是团队的灯塔
-----------------------
真正核心模型名称出来了,这是整个团队为此振奋的一刻,因为由此大家有了统一的语言:购电费,而不是各自用自己的基本属性随便组合成一个名词语言,比如合同部门是说电价,到了用电跟踪部门是电量,到了结算部门是购电费,他们只关心自己所辖边界內的重心,用不同方言在讲话,没有形成统一语言,这样的坏处是很大的,人类修通天塔到了上帝那里,上帝害怕了,搞了各种语言使得人们无法沟通,通天塔建设由此停止。

现在我们用JAVA表达购电费如下:

``````
public class electricityRates{...}

```````

失血模型与充血模型
----------------------------------
类名有了,内容呢?很多人平常是往里面填写,或者根据数据表字段导入,好吧,我们假设这里先不设计这个类,先设计数据表electricityRates,里面字段呢?

其实正是因为有了前面设计过程,这个类或数据表是我们的逻辑结果:

```````
电价+电量+用电时间段+用电规则 = 购电费electricityRates
```````

如果用数据表表达那么大概只能放入电价,电量,用电时间段等变量,但是用电规则恐怕要另外用代码实现,而我们用JAVA类表达:

``````
public class ElectricityRates{
电价一属性
电价二属性
根据用电规则计算购电费的方法

}
``````

正因为类不但有属性,还有方法行为,这个可以表达用电规则等约束。以后我们侧重用代码方式来表达领域模型,因为代码表现力很强,SQL简单也很强大,两者如同油画与国画,油画色彩绚丽,表现力比黑白颜色丰富得多。

这个类函数方法是需要一个输入和一个输出,输入应该是当时用电量,用电时段,那么我们在函数里查询逻辑规则中对应电价,输出购电费是多少钱。

当然也有将这段函数方法作为服务的函数方法去实现,传入一个VO值对象,如下面的ECostVO,VO里面是用电量,用电时段,逻辑规则是去查数据库,这样计算出电费,这是最普通的做法。

``````
public interface CalPurchaseElectricityService{

public float calElectricityRates(ECostVO eCostVO);

}
``````

这里面有一个很深含义的分歧,如果我们把原本属于领域模型的方法抽到服务里实现,这个领域模型如同被抽血一样就会成为失血模型,或称为贫血模型,同时也让服务变得臃肿,而将本属于模型的方法放入模型中称为充血模型。

那么如何区别哪些行为是否属于模型类的呢?这还是从逻辑概念的边界来看,我们前面分析出:
``````
电价+电量+用电时间段+用电规则 = 购电费
``````
同理:
``````
购电费=电价+电量+用电时间段+用电规则,
``````

也就是说,购电费由这几个部件组成,而计算购电费无疑是购电费这个模型的核心方法,涉及到这几个部件。

总结
---------------------------------
为了能在更大范围来确定领域模型的核心边界,我们从上面公式提炼出更普遍的公式:

``````
领域模型=实体+逻辑规则+逻辑流程
``````

电价和电量是购电上下文的主要实体,逻辑规则是某个用电时间段对应特别的电价,某个发电组对应特别的价格,逻辑流程是先有合同确定电价才有用电量跟踪。

根据这个规则,我们购电费类中还没有逻辑流程的约束,如何表达这样先后因果呢?用状态模式,记录好每个状态,约定进入下一个状态必须满足什么条件。

这个公式其实说明了我们分析大部分系统都是要分解为这三个部分,如果逻辑流程复杂了,我们可以使用专门的工作流引擎来实现;而逻辑规则复杂了,可以使用规则引擎和知识库专门的实现。那么是不是有了这些工作流引擎和规则库,是不是就不需要DDD这套了呢?其实一开始人们也是以为如此,工作流和规则引擎还出现在DDD之前呢,
但是人们还是用不好这些引擎和规则库,我认为原因在于我们这两章讲的内容,只有每个需求被我们经过分析,划分到这一公式,再开始使用相应的引擎或规则库才发挥作用,否则会用错,反而带来不便。

下一章