模型
-
由不同部分组成
-
用于特定目的
-
抽象的系统
-
认知工具
-
模型有几种表现方法(语言、代码、图解)
-
一个系统包含若干模型
通用语言是作为领域专家与软件专家之间的协作而演进的。
好的面向对象设计
- 单一职责原则:类只有一个职责
- 开放封闭原则:类应该对修改关闭,但是对扩展开放。
- 里氏替换原则:比如有个继承关系Person和Student。可以使用Person的时候,也可以使用Student。但是,当反射的时候,处理Student的方法可能不需要Person。
采用模式的建议
- 使用它,但未意识到使用它
- 听说它,阅读了一些知识,开始尝试
- 了解更多,开始明确地使用
- 开始热衷,并传播它
- 突然有所顿悟
- 学到更多,更成熟地、更含蓄地应用
- 随着时间流逝,看到缺点
- 对概念提出疑问(常常因为错误地应用了它)
- 忘记了它,或者增加了知识和经验
- 使用它,但是未意识到使用它
从以数据库为中心,过渡到以领域模型为中心(可以更加纯粹地使用面向对象技术)。
- 以领域模型为中心的设计更清晰,更忠实于领域抽象的实现,可维护性更高。
- 强大的领域模型是减少逻辑重复的有利工具。
解决方案轮廓:
- 问题/特性列表
- 逐个处理特性
- 代码是真实模型的最重要的表示
- 通过测试代码试验模型,并获得及时反馈
另一个维度:
- 孤立或共享的实例
- 领域模型实例化是有状态的还是无状态的
- 领域模型的完整实例化和子集实例化
领域驱动设计
领域模型不是特殊的图,而是图所要传达的思想。
不只是领域专家头脑中的知识,而是对这类知识严格地组织,和有选择的抽象。
图可以表示或者传达模型,文字也可以。
不是要建立符合现实的模型,而是概括地反映现实。
模型在领域驱动设计中的作用:
- 模型和设计的核心相互影响。
- 模型是所有团队成员使用的通用语言的中枢
- 模型是浓缩的知识。通过如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。
有效建模的要素
- 模型和实现的绑定。
- 建立一种基于模型的语言。
- 开发一个蕴含丰富知识的模型
- 提炼模型
- 头脑风暴和实验
知识消化不是孤立的活动,一般是在开发人员的领导下,由开发人员和领域专家组成的团队来共同协作。
信息的原始资料来自领域专家头脑中的知识,现有系统的用户,以及技术团队在相关遗留系统或者同领域的其他项目中积累的经验。
模型聚焦于需求分析。与编程和设计紧密交互。
当我们的不再局限于寻找实体和值对象时,我们才能充分吸取知识。因为业务规则之间可能会存在不一致。
更明确的设计:
- 程序员和其他相关人员都明确理解,明白它是重要的业务规则,而不是不起眼的计算
对象模型包括:属性、关系、行为和约束。
UML无法传达模型的两个重要方面,一是模型所表示的概念的意义,二是对象应该做哪些事情。
避免使用包罗万象的对象模型图。甚至不能使用包含所有细节的UML数据存储库。图要简单,只体现思想纲要。
任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。
任何负责修改代码的人则必须学会用代码表达模型。
每一个开发人员都必须不同程度地参与模型讨论并且你领域专家保持联系。
参与不同工作的人都必须有意识地通过UBIQUITOUS LANGUAGE与接触代码的人及时交换关于模型的想法。
模式:LAYERED ARCHITECTURE
分层:
- 用户界面层(controller)-向用户显示信息和解释用户指令
- 应用层(service)-定义软件要完成的任务,指挥表达领域概念的对象来解决问题。要尽量简单,不包含业务规则
- 领域层(实体bean)-表达业务概念、业务状态信息和业务规则。
- 基础设施层(一般也是service)-为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化工具,为界面层绘制屏幕组件等。
每一层分别设计,内聚。
各层之间松散连接,层与层的依赖关系是单向的。上层可以使用或者操作下层。如果下层想与上层通信,可以使用回调或者OBSERVERS。
领域模型是一系列概念的集合。领域层的软件构造反映了模型概念。
一个对象是用来表示某种具有连续性和标识的事物,还是用来描述某种状态的属性,是ENTITY和VALUE OBJECT之间的根本区别。
领域中还有一些方面适合用动作或者操作来表示,最好使用SERVICE。不要把操作的责任强加到ENTITY和VALUE OBJECT身上。SERVICE是应客户端请求完成某事。
MODULE是模型的一部分,应该反映领域中的概念。
使关联更容易控制:
- 规定一个遍历方向
- 添加一个限定符,以便有效地减少多重关联
- 消除不必要的关联
ENTITY有特殊的建模和设计思路。他们有生命周期,这期间他们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。
为了跟踪这些对象,必须定义他们的标识。
比如颜色就是VALUE OBJECT。
- 可以是其他对象的集合
- 可以引用ENTITY
- 经常作为参数在对象之间传递消息
- 可以作为ENTITY的属性
- 所包含的属性应该形成一个概念整体。比如街道、城市、邮编。
- 不关心使用的是它的哪个实体
- 两个VALUE OBJECT之间的双向关联完全没有意义
当我们只关心一个模型元素的属性时,应该把它归类为VALUE OBJECT。应该使这个模型元素表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。
SERVICE的特征:
- 与领域概念相关的操作,不是ENTITY或者VALUE OBJECT的一个自然组成部分
- 接口是根据领域模型的其他元素定义的
- 操作是无状态的(有副作用,但是不保持影响其自身行为的状态)
SERVICE并不只是在领域层中使用,要注意区分领域层的SERVICE和其他层的SERVICE。
领域层和应用层的SERVICE和基础设施层的SERVICE协作。应用层SERVICE和领域层SERVICE可能很难分清。
领域层的SERVICE要判断是否满足临界值。
账户之间的转账属于领域层SERVICE,因为它含有重要的业务层规则。
将SERVICE划分到各层中
- 应用层 - 资金转账应用服务
- 获取输入
- 发送消息给领域层服务,要求其执行
- 监听确认消息
- 决定使用基础设施层SERVICE发送通知
- 领域层 - 资金转账领域服务
- 与必要的账户和总帐对象交互,执行相应的借入和贷出操作
- 提供结果的确认(允许转账或者拒绝等)
- 基础设施层 - 发送通知服务
- 按照应用程序的指示发送邮件,和其他信息
SERVICE还有其他功能,可以控制领域层中接口的粒度,避免客户端与ENTITY或者VALUE OBJECT耦合。
大型系统中,中等粒度的,无状态的SERVICE更容易复用。因为在简单的接口背后封装了重要的功能。
细粒度的对象可能导致分布式的消息传递效率低下。
应用层负责对领域对象的行为进行协调,因此,细粒度的领域对象可能会把领域层的知识泄漏到应用层中。这样,应用层不得不处理复杂、细致的交互。
MODULE是一个传统的、较成熟的设计元素。
- 可以在MODULE中查看细节,而不会被整个模型淹没。
- 观察MODULE之间的关系,而不考虑内部细节。
MODULE从更大的角度描述了领域。
MODULE应该是低耦合、高内聚的。
MODULE不仅仅是代码的划分,也是概念的划分。
MODULE和较小的元素好像应该共同演变,实际上并不是这样。MODULE被用来组织早期对象。在这之后,对象在变化时不脱离现有模块定义的边界。重构MODULE要比重构类做更多的工作,更有破坏性,不会很频繁。
领域对象的生命周期
挑战:
- 在整个生命周期中维护完整性
- 防止模型陷入管理生命周期复杂性造成的困境当中
可以使用三种模式解决这些问题
- AGGREGATE - 聚合。通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有重要作用
- 在生命周期的开始阶段,使用FACTORY创建、重建复杂对象,封装内部结构。
- 在生命周期的中间和末尾使用REPOSITORY来提供查找和检索持久化对象并封装庞大基础设施的手段
FACTORY和REPOSITORY在AGGREGATE的基础上操作,将特定生命周期转换的复杂性封装起来。
在具有复杂关联的模型中,要保证对象更改的一致性是很难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间的互相干扰,导致系统不可用。
模型中要有明确定义的边界。
AGGREGATE就是一组相关对象的集合,把它作为数据修改的单元。每个AGGREGATE都有一个ROOT和一个边界。边界定义了AGGREGATE内部有什么。ROOT是AGGREGATE包含的一个特定ENTITY。对AGGREGATE而言,外部对象只能引用ROOT。而边界内部的对象可以互相引用。除ROOT以外的其他ENTITY都有本地标识。比如汽车就是一个AGGREGATE。
固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求时刻保持最新状态。通过事件处理、批处理或者其他更新机制,这些依赖会在一定时间内得以解决。但在每个事务完成时,AGGREGATE内部所应用的规则必须满足。
只有ROOT才可以直接通过数据库查询获取。其他所有对象必须通过遍历关联来获取。
应该将ENTITY和VALUE OBJECT分门别类地聚集到AGGREGATE中,并定义每个AGGREGATE的边界。在每个AGGREGATE中,选择一个ENTITY作为ROOT。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于ROOT控制访问,因此不能绕过它修改内部对象。
AGGREGATE划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。FACTORY和REPOSITORY都是在AGGREGATE之上执行操作。
当创建一个对象或整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多内部结构,则使用FACTORY封装。
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身没有承担领域模型中的职责,但仍是领域设计的一部分。提供一个封装所有复杂装配的接口,这个接口不需要客户引用要被实例化对象的具体类。在创建AGGREGATE时,要把它当作一个整体,并确保它满足固定规则。
好的工厂要满足两个要求:
- 每个创建方法都是原子的。生成的实例要处于一致的状态。
- 应该被抽象为所需的类型,而不是要创建的具体类。
工厂和参数耦合。
由于VALUE OBJECT是不可变的,所以对应的工厂所生成的就是最终形式。
随意的数据库查询会破坏领域对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。
REPOSITORY将某种类型的所有对象表示为一个概念集合(通常是模拟的),它的行为类似集合,只是具有更复杂的查询功能。在增加或删除对应类型的对象时,REPOSITORY的后台机制负责将对应的对象添加到数据库中,或者从数据库删除。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对AGGREGATE的ROOT的整个生命周期的全程访问。
REPOSITORY的优点:
- 提供了一个简单的模型,可用来获得持久化对象并管理他们的生命周期
- 使应用程序和领域设计与持久化技术(多种数据库策略、多个数据源)解耦
- 体现了有关对象访问的设计决策
- 很容易将他们替换为哑实现(dummy implementation),以便在测试中使用
基于SPECIFICATION(规格)的查询是将REPOSITORY通用化的好办法。客户可以使用规格来描述它需要什么,而不用关心如何获得结果。
也应该允许添加专门的硬编码查询。
将存储、检索和查询机制封装起来是REPOSITORY实现的最基本特征。
并不意味着每个类都需要一个REPOSITORY。
REPOSITORY通常不提交事务。
从领域驱动设计的角度来看,FACTORY和REPOSITORY具有不同的职责:FACTORY负责制造新对象,REPOSITORY负责查找已有对象。REPOSITORY应该让客户感觉那些对象好像驻留在内存中一样。对象可能必须被重建,但它是同一个概念对象,仍然处于生命周期的中间。
REPOSITORY也可以委托一个FACTORY来创建对象。
一般情况下,模型的精化、设计和实现应该在迭代开发过程中同步进行。