DDD:Domain-driven design

By on

前阵子在单位普及了一下DDD开发方法,下面是培训PPT和一些笔记:

注:PDF浏览需要HTML5浏览器支持

传统的开发方式多采用瀑布式,这种方法包含了一系列的阶段(如:统一过程),在细化分析阶段一般由业务专家提供大量的需求同业务分析人员进行交流,分析人员基于需求创建模型并作为指导传递给开发人员进行编码工作。领域知识是单向流动的,缺乏及时的信息反馈。
传统采用用例图描述需求,然后用它作为指导的开发方式,可能会造成设计很像事务脚本,将所有功能在方法中从头至尾的描述,当处理若干高度复杂的问题时,很有可能最终造成重复。
而之后的敏捷方法学,如XP。敏捷反对预先的、完全的设计,主张将需求转化为粒度较小的任务或用户故事,进而持续的、尽早的产生可交付物,尽快得到最终用户的反馈,以此进行循环迭代。但是敏捷也有一些局限性,虽然敏捷提倡简单灵活,但每个人对于这个概念的都有自己的观点。并且也缺乏一些真实可见的设计设计原则(大多可以认为是一些管理方法),所以在持续重构过程中可能会导致代码更难理解(过度设计)。
从某种程度上讲传统的瀑布式开发方式和敏捷式是两个极端。
而驱动领域设计与其他方法论的区别是它将软件系统看做业务流程的反映,而不是仅仅交付。更深的关注业务流程、业务术语和业务实践。技术关注点排在第二位。以纯净的领域模型来反映业务需求,代码即为需求文档。

应用层:组织和定义软件要完成的任务,这是与表现层或其他系统应用层进行交互的必要渠道,尽量简单,不包含业务规则与领域知识,只作为协调作用。
领域层:负责表达业务概念及业务规则。不包含任何其他与业务无关的逻辑,是业务软件的核心。
基础设施:为上层提供技术能力,为领域层提供数据持久机制,为应用层提供非业务领域内容的组件功能(如Mail等),Aop等等

由于领域专家和技术团队各自的工作性质不同,很有可能在沟通中各自倾向于自己擅长的语言方式,这就削弱了交流的效果,所以就需要一种通用语言。
在领域驱动开发中,领域模型就是这种公共语言的核心,并且确保在交流与代码中坚持使用这种语言,认识到对通用语言的更改就是对模型的更改。
目的就是将通用语言最为开发人员与领域专家的一种交流语言,开发人员可以使用这种语言描述系统中的工作、任务及功能,而领域专家可以使用这种语言讨论需求,开发计划等,最重要它们描述的是一致的。
UML:不可能完全的描述所有的领域需求(其实也是不主张这样做的),即便我们努力做到了,模型将会失去表现力,而且这样一来同步模型与代码工作量巨大。所以说UML属于分析过程中的产物(抛弃型的),是一种沟通解释的手段。相对比较适合表述一些核心业务。
领域驱动设计将模型作为统一协作语言,作为开发人员与领域专家之间的交流工具(宗旨)。这样就不可避免的需要对领域业务建模,模型真实反映领域业务,并且使最终产物代码真正反映领域(精髓)。

什么是模型
有不同部分组成
用于特定目的的
抽象的系统
认知的工具
模型有几种表示方法(比如语言,代码,图解)
一个系统中保含若干个模型

当然我们再传统开发中也一直在使用模型,但我们所使用的旧模型与DDD思想创建的新模型之间有一个区别的,即旧模型主要关注基础架构和技术概念,新模型没有将注意力分散到这些地方,而是完全集中在核心领域、领域概念以及当前领域问题上。

传统的开发中数据对象(ORM、表映射对象)
贫血对象(POCO)
充血对象

值对象举例:如:
同处一室的两个室友,都订购了Vancl的货物,那么是否要区分他们是否在同一个地方并不重要。所以这是一个Value Object。
同样是这对室友,他们分别打电话申请宽带服务,宽带运营上需要知道他们是属于同一个地点,在这种情况下地址是一个Entity。

边界定义了aggregate内部有什么,根是唯一允许外部对象保持对其引用的元素,而其内部对象间可以互相引用。

根据有全局标识
根内部的entity据有本地标识,在根内部唯一
根的外部不得引用其内部对象,只能通过根传递出去,只能临时使用(或value object作为副本传递,发生变化不得有副作用),不能保持引用。
只有根直接通过数据库查询,其他的对象必须通过遍历
根内部可以保持对其他根的引用
级联删除,父子对象的关系
提交根或其内部对象的修改时,整个根的所有规则必须都通过,即约束完整性

当领域中的一些重要的进程或转换操作不是实体和值对象本身的职责时,把操作作为一种独立的接口加入模型,并声明为服务。根据模型中使用的语言来定义接口,保证操作名是通用语言的一部分,并使这个服务变成无状态。服务应该具有确实的领域上意义,它可以代表一个领域提供给外部系统的一个功能,也可以代表了领域中一个在多个实体和值对象间协作完成的操作,而这个操作不属于任何一个实体或值对象。

服务是一个容易被滥用的概念,在贫血模型的设计里面,就把所有的功能都放在服务中完成,而让实体和值对象不再拥有领域功能,变成了只包含数据的贫血变种。这样的设计会将领域职责与领域主体分离,不利于抽象,很容易造成重复的设计,也就是为什么我们的代码里会充斥着方法的拷贝,因为没有抽象到领域行为的粒度,还是以功能为主的函数式方式。

装配对象并不适合承担复杂的装配逻辑,如此一来可能会造成难以理解的拙劣设计,而如果让客户代码负责构造对象,又会使客户的设计陷入混乱,破环领域模型的封装性,导致耦合的产生。

工厂和仓储都不是领域中已有的概念,但是工厂里包含全是领域逻辑,而没有数据访问逻辑,而仓储则全是数据访问逻辑而尽量不要包含领域逻辑。我们可以让工厂创建一个对象,让仓储持久化这个对象,仓储也可以读取已有领域对象的持久化数据后,让工厂重建领域对象。

问题:当具有复杂行为的软件缺乏一个良好的设计时,重构或元素的组合就变得很困难。一旦开发人员不能肯定的预知计算的全部含义是,就会出现重复。当 设计元素都是整块的而无法重新组合式,重复就是一种必然的结果。如果软件没有一个条理分明的设计,那么开发人员不仅不愿意仔细分析代码,而且修改代码也可 能产生问题——要么加重代码的混乱状态,要么由于某种未预料的依赖性而破坏了某个结构。任何一个系统的这种不稳定性是我们很难开发出来易维护的丰富功能, 而且也限制了重构和迭代式的精化。
目的:使设计让人们乐于使用,而且易于作出修改。
柔性设计是一些设计和实现模型领域开发的一些具体模式,它能够揭示深层次的底层模型,并把潜在的部分明确展现出来,来指导突破复杂性带给我们的限制。
1.Intention-Revealing Interfaces (释意接口)
对象的强大功能之一在于它能把所有细节封装起来,这样客户代码会非常简单,即可以用高层概念来解释。如果对象接口没有能够让客户开发人员了解到有效的信息,势必开发人员就会深入研究内部机制,造成“认识过载”。
如 果开发人员为了使用一个组件而必须去研究它的实现,那么就失去了封装的价值。当某个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不 根据其实现来推测其用途时,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经 被误用了,两位开发人员的意图也是背道而驰的。
在命名类和操作时要描述他们的效果和目的,而不要表露他们是通过何种方式表达到目的的。这样 可以是客户开发人员不必去理解内部的细节。这些名称应该与通用语言(ubiquitous language)保持一致,以便团队人员可以迅速的推断出它们的意义。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上 来思考他(TDD)。
2.Side-Effect-Free Funtion(无副作用的函数)
 一个操作 (命令或查询)如果还会调用其他操作,而其操作还会跟另外的操作产生调用关系,一旦形成这种任意深度的嵌套,就很难预测这个操作将要产生的结果,这样第二层乃至第三层调用所造成的影响可能并不是开发人员有意而为之的,于是它就变成了完全意义上的副作用(即意外的结果)。这样的副作用是任何系统都不想产生的。
多 个规则或计算组合的相互作用所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及所有派生操作的实现。如果开 发人员不得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全的预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了 系统行为的丰富性。
 尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格的把命令(引起明显的状态改变的 方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂的逻辑移到Value Object中,这样可以进一步控制副作用。
3.Assertion(断言)
 如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量互相调用关系的系统里,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行。封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。
把操作的后置条件和类及Aggregate的规定规则表述清楚。如果在你的编程语言中不能直接编写Assertion,那么就把他们编写成自动的单元测试。还可以把它们写道文档或图中(如果符合项目开发风格的话)。
寻找在概念上内聚的模型,以便使开发人员更容易推出预期的Assertion,从而加快学习过程并避免代码矛盾。
使用“契约式的设计”,对给出类和方法的“断言“使开发者可以肯定会发生的结果。即:后置条件描述了一个操作的副作用,结果。前置条件表明了需要满足的条件。固定规则规定了结束时对象的状态。
4.Conceptual Contour(概念轮廓)
 如果把模型或设计的所有元素都放在一个整体的大结构中,那么他们的功能就会发生重复。外部接口无法全部给出客户可能关心的信息。由于不同的概念被混合在一起,他们的意义变得很难理解。
而另一个方面,把类和方法分解开也是毫无意义的,这会使客户更复杂,迫使客户对象去理解各个小部分是如何组合在一起的。更糟糕的是,有的概念可能会完全丢失。铀原子的一般并不是铀。而且,力度的大小并不是唯一要考虑的问题,我们还要考虑力度是在那种场合下使用的。
把 设计元素(操作、接口、类和Aggregate)分解为内聚的单元,在这个过程中,你对领域中的一切重要划分的直观认识也要考虑在内。在连续的重构过程中 观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层Conceptual Contour。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
我们的目标是得到一组可以在逻辑上组合起来的简单接口,是我们可以用Ubiquitous Language把他们表述出来,并且是那些无关的选项不会分散我们的注意力,也不增加维护的负担。
Intention-Revealing Interface使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。
Side-Effect-Free Function和Assertion使我们可以安全的使用这些单元,并对它们进行复杂的组合。
Conceptual Contour的出现使模型的各个部分变得稳定,也是的这个单元更加直观,更易于使用和组合。
5.Standalone Class(孤立的类)
 即使是在Module内部,设计也会随着依赖关系的增加而变的越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显示的引用增加的负担更大。
低耦合是对象设计的一个基本要素。尽一切可能保持地耦合。把其他所有无关概念提取到对象之外。这样类就变成完全孤立的了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大的减轻了以理解Module而带来的负担。
尽力把最复杂的计算提取到Standalone Class中,可能实现此目的的一种方法是把具有紧密联系的类所包含的Value Object建模出来。
低耦合是减少概念过载的最基本办法。孤立的类是低 耦合的极致。
6.Closure Of Operation(闭合操作)
 在 适当的情况下,在定义操作是让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作 的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入其 他概念的任何依赖。

Wiki:语言就广义而言,是一套共同采用的沟通符号、表达方式与处理规则。符号会以视觉、声音或者触觉方式来传递。严格来说,语言是指人类沟通所使用的语言-自然语言。一般人都必须通过学习才能获得语言能力。语言的目的是交流观念、意见、思想等。
通过DSL的方式来调整我们的业务