领域驱动系列(1)--域模型

一、什么是域模型?

引用: https://developer.aliyun.com/article/2255

域模型(domain model)英文又称为问题域模型(problem space model)。维基百科(Wikipedia)对它的定义是” A conceptual model of all the topics related to a specific problem” . 可以翻译成: “域模型是针对某个特定问题的所有相关方面的抽象模型”。 这个定义有几个要点:第一是“特定问题”, 也即是说域模型是针对性某个问题域而言的, 脱离的这个特定问题,域模型的构建其实不存在一个最优或者是最合理的构建。  第二是抽象, 域模型是一个抽象模型, 不是对某个问题的各个相关方面的一个映射, 也不是解决方案的构建。 这一点我后续会展开来讲。  

关于域模型, 经常会看到大家把逻辑数据模型(logical data model) 或者是物理数据模型(physical data model)和域模型混为一谈。 甚至有同学把数据里的表结构抽取出来作为域模型来研究。 其实这是域模型的最大误区。 数据模型实质上都归属于结果域模型(solution space model), 是对某个问题域的解决方案的一个描述, 实质上是对解决方案的一个具体描述。

另外一个常见的误区和领域驱动设计(DDD, Domain Driven Design)有关。 我个人对DDD比较推崇, 但是DDD里提到的域模型实质上是结果域模型(Solution Space Model), 不是问题域模型。 我在这个系列的文章里集中介绍的是问题域模型的构建, 而不是结果域模型的构建。这两者的区别在于前者的建立主要是为了统一我们对未知世界的了解, 也就是说我们需要统一思想, 搞清楚我们要解决什么问题和问题的本质。 而后者的主要是想解决针对近些年来敏捷开发模式中所普遍存在的对领域认知不完整而导致设计不合理的问题。 前者是一个对未知方向的探索过程,适用在一个相对较为模糊的命题,产出是对语言,边界和思路的统一。后者是一个方法论,适用于具体一个项目, 产出是一个项目的数据模型。

总结一下:(问题)域模型是为了准确定义需要解决问题而构造的抽象模型。  特别值得强调的是域模型的功能是统一认知对要解决的问题有一个完整,规范,并且是一致的认识。 

二、域模型的主要概念

引用:https://developer.aliyun.com/article/6384

域模型是为了准确定义需要解决问题而构造的抽象模型。 这个抽象模型中最核心的概念就是实体(Entity)。

域(domain):我们讨论的域是问题域。就是我们要解决问题的边界。

子域(subdomain):一个大的问题域又会被递归的分割为多个小的问题域。

语境(context):是一个特定人群在讨论的问题域是所形成的上下文。 这里要强调一个概念, 特定人群不是以团队或者是项目为边界划分的人群, 而是以知识为边界来划分的人群。 也就是说上下文不是普遍存在的, 而是存在于一个人群内部的,并且这些上下文大多是以隐形知识(Tacit Knowledge)的方式而存在的。  什么是隐形知识呢, 就是还没有被总结整理归纳沉淀的知识。

特定语境(Bounded context):是把上下文限定到某个特定的边界之内。 这个边界是由某个特定人群和他们所讨论的问题子域来决定的。

语境映射(Context Mapping):不同的语境之间会有交互, 那么从一个语境到另一个语境的翻译过程就是语境映射。

域语言(Ubiquitous  Language):是一个团队在某个特定语境之下建立的交流语言。 这个语言有两个要求:

    第一:特定语境。 这个语言不是自然语言, 而是在某个语境之内的一个特定语言。 任何一个团队或任何一名同学都可能横跨多个域,这些域内的各种概念可能不是显性化的,并且某些名词有可能会是重叠的。 

    第二:准确无歧义。也就是在这个语境之下, 交流双方任何一个成员对某个名词,描述,上下文的理解和另一个成员的理解是一样的。

域语言这个概念是Eric Evans发明的。 可惜有点用词不当。 他给这个概念起名为 Ubiquitous Language。 而Ubiquitous 是无时无处不在的意思,也就是Omnipresent。 这与域语言的概念恰恰相反。 造成了大家理解上的困难。  个人认为应该叫“Domain Language”更合适。  

实体(Entity):是一个可以唯一标识的个体。

    第一, 实体可以是抽象也可以是客观存在的

    第二,我们讨论的任何个体是有其生命周期的。 它有个从不存在到存在,然后最终到不存在的过程。 首先说实体是有状态的。 并且往往实体在这些状态自己的转化是有实体之外的事件(Event)所触发的。了解实体状态和触发事件其实是个认知一个实体非常关键的一环这里的另一层含义是实体不是脱离个体而存在的运行规律。 用程序来打比方, 我们的实体不是计算逻辑, 而是被这些逻辑所操纵的对象。 从现实世界的角度来说, 实体可以是万物。 而主宰万物运行的自然规律“道”却不是实体。

    第三,讨论的任何个体在我们的问题域之内是可以被单独的分辨出来的。这里我想强调一点,就是个体在一个特定的问题(子)域中可以被分辨, 而不是没有任何限定范围内的可以被分辨出来(Entity is Individually identifiable within the problem (sub)domain)。事实上我看到最频繁的域模型设计纠结就发生在对实体的认知上。在解决一个问题的时候到底我们需要考虑什么实体经常是域模型设计最关键的一环。 

触发事件(Domain Event):是引起实体状态变化的事件。 触发事件客观存在, 有生命周期, 可以单独分辨,所以它本身也是一个实体。 但是触发事件并不一定需要被显性建模。 往往某些触发事件比如说用户注册,被当作实体外的运行规律的一部分。 但是我个人建议大家还是要描述触发事件,虽然不一定要在最终的Model Diagram里画出来。

数值对象(Value Object):简单来讲就是常数对象, 比如说一个数字, 一段文字, 某一组参数。这些常数可能递归的组成一个复杂的object。 但是从问题域的角度上来说, 这些常数不存在一个生命周期。

   数据对象可以是一个相对的概念。  在一个问题领域的数值对象可能是另一个问题领域的一个实体。

实体类型(Entity Type):实体类型是某一类实体的聚类。 事实上我们很少把建模精力花在研究某个实体上,我们大多数时间在研究实体类型。 简单来说我们是在研究Class的定义,而不是研究某个object的定义。  这又是领域建模过程中又一个纠结点。 因为我们在谈论一个实体的时候, 我们是没有歧义的。 但是一旦抽象到类型,我们抽象的边界就是一个很大的问题。 

关系(Relationship):是实体类型(相当于Class), 而不是实体本身(相当于Object),之间的关系。所以关系也会有属性, 也有实例。关系实例的特例有时候不能被关系类型所不能包涵的的情形。

场景(Scenario):场景是问题域中的一个问题实例。 也就是说我们要解决的具体问题。 对Scenario的深度理解建议大家参考一下Scenario-Based Engineering的书籍。  Scenario生成是把一个抽象的问题转化成生动的实例。

核心场景(Core Scenario):是问题域中的不可以牺牲的问题实例。 牺牲场景,也就是不把某一类场景作为有必要解决的问题, 是一个既需要经验又需要Vision的难题。我们用户痛点到底在哪里,解决这些痛点是否对我们的未来有决定性意义。

三、域对象

引用: https://blog.csdn.net/chz_cslg/article/details/23958033
构成域模型的基本元素就是域对象。域对象(Domain Object),是对真实世界的实体的软件抽象。域对象还可以叫做 业务对象(Business Object)。

1.域对象的分类

    实体域对象:可以代表人、地点、事物或概念。
    过程域对象:代表应用中的业务逻辑活流程。
    事件域对象:代表应用中的一些事件。

2.域对象之间的关系

    关联:关联指的是类之间的引用关系,这是实体域对象之间最普遍的一种关系。关联可以分为一对一、一对多和多对多关联。
    依赖:依赖指的是类之间的访问关系。
    聚集:聚集指的是整体与部分之间的关系。
    泛化(也称一般化):泛化指的是类之间的继承关系。
 

四、域模型的分类

引用: https://www.cnblogs.com/chenzhao/archive/2012/08/13/2636179.html

域模型初步分为4大类:
    1. 失血模型
    2. 贫血模型
    3. 充血模型
    4. 胀血模型

1. 失血模型
失血模型简单来说,就是domain object只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成(又称
TransactionScript),这种模型下的domain object被Martin Fowler称之为“贫血的domain object”。

2. 贫血模型
简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。
Service(业务逻辑,事务封装) --> DAO ---> domain object
这也就是Martin Fowler指的rich domain object。

这种模型的优点:
    (1): 各层单向依赖,结构清楚,易于实现和维护
    (2): 设计简单易行,底层模型非常稳定
这种模型的缺点:
    (1): domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
    (2): Service层过于厚重

3. 充血模型
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑)
,而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
 Service(事务封装) ---> domain object <---> DAO

这种模型就是把第二种模型的domain object和business object合二为一了。

在这种模型中,所有的业务逻辑全部都在Item中,事务管理也在Item中实现。
这种模型的优点:
    (1): 更加符合OO的原则
    (2): Service层很薄,只充当Facade的角色,不和DAO打交道。
这种模型的缺点:
    (1): DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
    (2): 如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
    (3): 考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义
一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript
。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。

  1.事务我是不希望由Item管理的,而是由容器或更高一层的业务类来管理。
  2.如果Item不脱离持久层的管理,如JDO的pm,那么itemDao.update(this); 是不需要的,也就是说Item是在事务过程中从数据库拿出来的,并
且声明周期不超出当前事务的范围。
  3.如果Item是脱离持久层,也就是在Item的生命周期超出了事务的范围,那就要必须显示调用update或attach之类的持久化方法的,这种时候
就应该是按robbin所说的第2种模型来做。

4.胀血模型
基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装
事务。
domain object(事务封装,业务逻辑) <---> DAO
似乎ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。
该模型优点:
    (1): 简化了分层
    (2): 也算符合OO
该模型缺点:
    (1): 很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
    (1): domain object暴露给web层过多的信息,可能引起意想不到的副作用。

 总结:

在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。 我认为权衡下来,第二模型的第一变种是相对最好的解决方案,不过它仍然有一定的不足,在这里我也希望大家能够提出更好的解决方案。

partech 提出了 实体控制对象 和 实体对象 两种不同层次的 Domain Object ,由于 Domain Object 可以依赖于 XXXFinderDAO,因此,也就不存在“大数据量问题”,因此,整个 Domain 体系,对于实际业务表述的更为完整,更为一体化。我非常倾向这种方式。

一般是这样的顺序:
Client-->Service-->D Object-->DAO-->DB
至于哪些该放在哪里,基本有这样的原则:(就是robbin的第二种了)
DO封装内在的业务逻辑
Service 封装外在于DO的业务逻辑当然如果业务逻辑简单或者没有的话也可以:

Client-->D Object-->DAO-->DB

对于第二种的第一个变种固然是个好办法,但如Robbin所说也有缺陷如果有多个Servcie要调用DAO的话,就有问题了。合并也意味中不能很好
的重用,说到底就是粒度的问题,分得细重用好,但类多、结构复杂、繁琐。分得粗(干脆用一个类干所有的事)重用差,但类少、结构简单

参考: 

https://developer.aliyun.com/article/2255

https://developer.aliyun.com/article/6384

https://blog.csdn.net/chz_cslg/article/details/23958033

https://www.cnblogs.com/chenzhao/archive/2012/08/13/2636179.html

原文地址:https://www.cnblogs.com/sfnz/p/14162405.html