领域模型管理基础点汇总(1)
在我们使用领域模型的时候,很少会创建实际领域模型类,然后使用去使用它们那么简单。我们会使用相当数量的基础架构代码来支持领域模型之间的领域逻辑关系。在这里运行的基础架构的这部分称为领域模型管理(Domain Model Management)简称:DMM。
问题一、基础架构代码放在那里?
随着基础架构代码的增长,找到一个处理它的优良架构越来越重要,在架构系统中,基础架构代码该如何放置,可以把一些基础代码放在我们的领域类里面,当然这是我们所尽量避免的。因为领域模型应该表示应用程序所处理的核心业务概念,对于对于想大量使用其领域模型的应用来说,保持这些类干净、轻量级、易于维护是我们所共同追求的。保持领域模型类完全不含基础架构代码——通常称为使用POJO(java)/POCO(Clr Objects)领域模型,但是经证实这种应用是有问题的。最终往往是导致采用笨重的、低效率的变通方法来解决问题——而且有些功能这种方式根本不能实现。
所以说,我们必须将适当的基础架构代码放入到领域模型类中,这样的话,我们就需要一个权衡利弊的情况,我们应该尽量在领域模型类中放必不可少的基础架构代码,绝不超过这个限度,虽然这样使我们的领域模型微微发胖,当时换来效率的提高以及使得一些必要的领域模型的功能可能实现,这样方式显然是利大于弊,这里的问题就是我们的这个限度该如何把握呢。
下面新建立一个应用,我们把基础代码全部放置到领域模型类中,然后我们一步步的重构它,使用我们众所周知的、可靠地、真正面向对象的设计模式,重构成具备相同的功能,但是基础架构代码不会弄乱的领域模型类,当然这样更能实现我们的更大的一个好处就是:基础代码的重用。
重构废领域模型
大部分的领域模型运行时是管理是基于拦截的——当你在代码中访问领域模型对象时,你所有的对对象访问都会根据相应功能的需要被拦截下来。
一个明显的例子是:脏跟踪(dirty tracking)。它可以用于应用的很多部分,以了解一个对象什么时候已经被修改、但是仍未保存(它处于“脏”状态)。用户界面可以利用该信息提醒客户端是否打算放弃任何未保存的修改,而持久化机制可以利用他来辨明那些对象时真正需要被保存到持久化介质中的,从而避免保存所有对象。
脏跟踪的一种方法是保持领域对象最初的、未修改版本的拷贝,并且每次想知道一个对象是否已经被修改的时候去比较它们。这个方案的问题是即浪费内存,又慢。一个更有效的方法是拦截对领域对象Setter方法的调用,以便每当调用对象的已给Setter方法的时候,都为该对象放置一个boolen类型的脏标记。
举例:一种方式将它放在一个字典结构中,对象和标记分别为键和值。这样做的问题在于,我们必须让程序中所需要它的部分都能访问到这个字典。也即是公共基础架构中,需要访问该字典包括:用户界面和持久化机制这样两个截然不同的部分。
将该字典放在这些组件的任何一个内部,都会使其它组件难以访问它,在分层架构中,有一个原则我们需要记住:底层不能调用其上层(除了中心领域模型,它常常处于一个公共的、垂直的层里面,能被其它所有的层调用),因此要么把字典放在需要访问它的最底层如上图,要么放在公共的、垂直的层里面,如下图,但是这两种选择都引起了应用组件不必要的耦合和不均衡的责任分配。
虽然这两种方式都保持了领域对象的干净,但是都未达到真正的解耦和系统责任分配,想想领域对象中数据是否为脏,是它的一个状态,为了顺应责任分配的面向对象的选择,我们将脏标记放到领域对象本身中去,这样每个领域对象都带有一个波尔值的脏属性,来表明它是不是脏的,任何组件想知道一个领域对象是否为脏的时候,可以直接访问它就行。
这样的好处是,我们部分基础架构代码能放在领域模型中,其部分原因是我们希望从应用的不同部分都能拥有这些功能,而不会过度的增强耦合。更能实现责任分配,用户界面部分不该知道如何持久化组件询问标志,并且,我们宁愿分层的应用架构中设计尽可能少的垂直层。
我们C#代码实现它
public class Person : IDirty
{
protected string name;
public virtual string Name
{
get { return name; }
set
{
if (value != name)
((IDirty)this).Dirty = true;
name = value;
}
}
//公开接口中的Dirty属性,
private bool dirty;
bool IDirty.Dirty
{
get { return dirty; }
set {dirty=value;}
}
}
interface IDirty
{
bool Dirty { get; set; }
}
上面的类显示的实现了接口(IDirty)中定义,也就是说我们在设定值的时候访问了接口中定义的Dirty属性,并且在本省类中进行了公开,当这样有一个弊端:在上面的类中我们只有一个属性(Name),如果我们要有更多的属性,我们将不得不在每个都Set方法中重复这行代码,并将比较对象改成相对应的属性。
一方面,应用中所有的组件只要用到对象模型都可以访问的脏跟踪信息。从不需要随时保留对象未经修改版本的拷贝来说,这种做法还是节省资源和快速的。
另一方面,这样领域模型类不在严格的管住商业业务。在领域模型类中忽然添加大量的基础架构的代码,使得实际的业务逻辑代码变得更加难以理解,并且类本身变得更加不可靠、难以变更、难以维护。
如果脏跟踪是唯一必须插入到领域模型类中(或至少这样做有好处的)的基础功能,我们到还可以接受的,但是在真正的领域设计架构中我们还要加入更多的功能需求,举几个简单的例子:
1、脏跟踪:对象持有一个脏标志,表示对象是否已经修改,但仍未保存。基于拦截或新成员的引入(脏标记属性以及getter、setter方法)。
2、延迟加载:对象第一次被访问的时候才加载对象状态,基于拦截和新成员引入。
3、初始值跟踪:当一个属相被修改(但尚未保存)的时候,对象保留未修改值的拷贝。基于拦截和新成员的引入。
4、反转(Inverse)属性管理:双向关系中的一对属性自动保持同步。基于拦截。
5、数据绑定:对象支持数据绑定接口,从而在数据绑定情况下使用。基于新成员的引入(数据绑定接口的实现)。
6、复制:对象的变更分发到监听系统。基于拦截
7、缓存:对象(或者至少是他们的状态,参加备忘录模式拷贝到本地存储器中,随后的请求可以在本地存储器中查询,而不用到远程数据源,基于拦截和新成员的引入)
随着这些功能的增加,可能更多的功能——都依赖领域类中的基础架构代码,干净、易于维护的领域模型的忽然变得的似乎遥远起来。
到现在真正的问题已经出来了,不允许在领域中添加基础架构代码——没有充分利用面向对象——由此带来的限制我们已经看过:在应用的不同组件之间共享基础架构功变得更难,并且我们常常以低效率的”变通方法“而告终,将”贫血“领域模型,基础架构代码不能充分利用面向对象的潜力。如果我们把所有的基础架构代码放置到领域模型类中,这就造成了”肥胖不堪“——难于使用和维护。
下面我们一步步的解决这样的问题:
使用一个公共的基础架构基类
可以把基础架构代码放在一个公共的基类中去,领域模型类继承该基类。然后,这个想法的主要问题在于,他对引进新的基础架构成员(像脏标记)到领域模型类中是有用的,但是它没有为我们提供拦截,而拦截是我们想要支持的许多功能要求的。
基类方法的另一个问题是,当需要调整功能的应用方式的时候,它不能提供必要级别的粒度。如果不同的领域模型类有不同的基础架构需求,那么我们就会遇到问题。他们也许能用一组不同的基类(也许互相继承)来解决这些问题,但是如果我们遇到单一继承,要是领域模型本身也想使用继承,就没法这么做了。
通过一个例子类展示我们刚才提到的两个问题:
假设我们有一个Person类,已经能够进行脏跟读,还有一个Employee类同样具备脏跟踪和懒加载能力。最后,Employee类需要继承person类。
如果我们欣然把基础架构代码放进领域模型中,那么我们有一个像上面代码中的person类,来新建一个Emloyee类:
public class Employee : Person, ILazy
{
public override string Name
{
get
{
if (!loaded)
{
((ILazy)this).loaded = true;
}
return base.Name;
}
set
{
if (!loaded)
{
((ILazy)this).loaded = true;
}
base.Name = value;
}
}
private bool loaded;
bool ILazy.loaded
{
get { return loaded; }
set
{
if (loaded)
return;
loaded = value;
}
}
private decimal salary;
public decimal Salary
{
get
{
if (!loaded)
{
((ILazy)this).loaded = true;
}
return salary;
}
set
{
if (!loaded)
((ILazy)this).loaded = true;
if (value != salary)
((IDirty)this).Dirty = true;
salary = value;
}
}
}
public interface ILazy
{
bool loaded { get; set; }
}
从上面的代码可以看出,领域模型类中的实际业务内容有被淹没的危险,基础代码的膨胀已经开始慢慢的开始蔓延,一个崭新的超胖领域模型就这样诞生了,为了遏制住这种趋势,我们考虑别的方法进行重构。
首先创建一个DirtyBase基类提供脏数据标示,然后我们创建一个LazyBase提供了一个加载标示。也就是形成这两个接口,然后我们让Person类继承DirtyBase,Employee类继承DirtyBase和LazyBase。
上面的方面貌似能够实现基础架构代码和业务领域逻辑代码的解耦,但是我们这里采用的是接口规范,接口只能算是类的约束,并不能确切的实现基本功能,而且当伴随着基础架构的功能的增多,很明显这不是一种好的解决方案,当然我们可以采用抽象类来实现,但是在c#和java这种单继承的面向对象语言中,又制约着我们这样做。
但是,面向对象提供了一个绝好的方法,能同时解决上面我们所描述的两种矛盾,我们来看延续思路:
在领域模型中架构代理类
我们就是创建继承领域模型的子类——每个(具体的)领域模型类都有一个新子类。
这个子类能拦截对其基类成员的访问,通过重写(Override)基类的成员来实现,重写的版本首相执行拦截相关活动。然后将执行传递给基类的成员,实现解耦。
当然我们也不可能为每一个领域模型添加同样功能(比如脏标记)的子类,仍然可以把他们放在所有领域都能继承的公共基类里面;而少数类的专属功能,则就可以将必要的成员放置在相关的子类中:
公共基类实现了所有领域模型类公有的基础架构代码。不是所有领域公用的那些功能则使用接口。
领域模型类脱离了基础架构代码。
通过代理子类重写领域类成员来提供拦截。它们还实现了基础架构成员,这些成员不是所有领域类公有的。
用子类的方式来提供拦截,本质上是一种设计模式(代理模式)的一种实现,还有一种变种使用领域模型接口——一个领域模型类一个接口——接口能被dialing和领域类同时实现。
代理类在内部属性中持有一个领域类实例,它实现领域接口的时候把所有调用都转发给背后的领域对象。实际上是GOF中的代理模式——只不过使用子类来实现。
使用子类的好处是,你不必非要必须一堆没有其他用处的领域接口,同时还有一个好处,代理子类中的代码都能使用this关键字调用继承领域类的代码。
而基于接口的代理中的代码将不得不通过内部属性来引用领域对象。子类还能访问受保护的成员,而基于接口的代理则要通过反射才能访问这些受保护的成员,并且如果领域类中有一个方法返回“this”的引用,调用代码就会得到一个”未代理“的领域类实例,这意味着脏读和懒加载之类的功能在这个实例中时不可用的。
至此我们已经顺利解答完第一个问题。
问题二、我们如何形成规范的POJO/POCO领域模型?
经过问题一的整理规范,我们已经基本实现了基本功能逻辑代码位置的放置,我们把上面的代码重新整理一份完整的。
public class Person : DomainBase
{
protected string name;
public virtual string Name
{
get { return name; }
set { name = value; }
}
}
public class Employee : Person
{
protected decimal salary;
public virtual decimal Salary
{
get
{
return salary;
}
set
{
salary = value;
}
}
}
public class DomainBase : IDirty
{
private bool dirty;
bool IDirty.Dirty
{
get
{
return dirty;
}
set
{
dirty = value;
}
}
}
interface ILazy
{
bool Loaded
{
get;
set;
}
}
interface IDirty
{
bool Dirty { get; set; }
}
public class PersonProxy : Person
{
public override string Name
{
get
{
return base.Name;
}
set
{
if (value != this.name)
((IDirty)this).Dirty = true;
base.Name = value;
}
}
}
public class EmployeeProxy : Employee, ILazy
{
public override string Name
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;
}
return base.Name;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;
//
}
if (value != this.name)
((IDirty)this).Dirty = true;
base.Name = value;
}
}
public override decimal Salary
{
get
{
if (!loaded)
{
((ILazy)this).Loaded = true;
//perform lazy loading...
//(omitted)
}
return base.Salary;
}
set
{
if (!loaded)
{
((ILazy)this).Loaded = true;
//perform lazy loading...
//(omitted)
}
if (value != this.salary)
((IDirty)this).Dirty = true;
base.Salary = value;
}
}
private bool loaded;
bool ILazy.Loaded
{
get
{
return loaded;
}
set
{
if (loaded)
return;
loaded = value;
}
}
}
我们来梳理一下经过这几次重构之后该架构所具有的特点:
1、我们能将有关基础架构代码分布到领域模型中(确切的是其代理类),使我们应用中所有部分都很容易访问,并使它能用面向对象和高效率的方法实现
2、可以动态的扩展新的基础架构代码,比如添加新接口,并且我们实际的领域模型类中的代码,仍能够保持干净、完全集中在他们需要关注的业务方面。
3、灵活搭建某个领域模型所需要的基础结构功能,并且在不改动领域对象的基础上,只需要增加每个领域类需要的基础架构代码,正符合OOP所提到的开闭原则
4、我们能随时构建AOP等切面功能,在不更改领域对象的前提下。
当然,按照POJO/POCO的严格定义,我们的领域模型类不应该继承基础架构基类。不过,我们很容易改过来,只要保证我们所有的逻辑公共基类移到代理子类中去,就可以得到一个完全的POJO/POCO领域模型,当然这样会造成有些功能的重叠,如果不强求按照POJO/POCO定义,我们完全可以使用基类,此时领域模型类的代码仍然不包含任何实际的基础架构代码。
因此,到目前为止我们已经整理出来了一个架构,让我们既能实现POJO/POCO的领域模型类,又能实现将基础架构代码分布到领域模型中去的目标。如果我们的目标仅仅是避免领域过于肥胖,而不强求符合POJO/POCO定义,那么我们就能走“半POJO/POCO”的路线,用一个公共基类来节省一些工作。
当然这种方式也并未完美的,两点需呀注意:
1、为了让子类(代理)能够重写领域模型类的成员,并提供拦截,所有的领域模型成员(或者至少打算拦截的那些成员)必须是虚拟的,也就是里面的成员属性必须有Virtual关键字描述
2、既然想让代理子类实现实例,我们必须替换所有用到新建领域模型类实例的地方,把他们改成创建相应的代理子类的实例,当然几个领域模型改起来可能会不费事,但是当领域模型增多之后,更改这很显然不是很好的办法。
也就引出了我们的第三个问题:
问题三、如果引用模型代理来的实例?
为了避免上面所述的大量代理类的实例的更改,只有一条出路,那就是一开始就避免new关键字来实例化对象,也就是将new对象的功能给封装起来,达到封装变化点,使得各个易变对象(领域模型的代理类)能够独立实例,到此,如果使用过GOF大师写的设计模式的时候,你会想到“抽象工厂”。
我们用抽象工厂模式,调用一个工厂类的Create()方法,而不是任何客户代码使用“new”来创建实例,而是通过调用工厂中的Create()方法来负责创建相应的实例对象,并且在返回新的实例之前会对其做一些额外的相关设置。
使用抽象工厂来实例所有的领域对象的一个重要理由是java和C#语言的局限性,因为他们不能像C++一样能重载(overload)成员取用运行子,不能像Objective-C一样能改变“New”关键字的行为,也不能像Ruby一样在运行时修改类。
但这里面有个点需要注意:继承反射
在应用的时候我们可能觉察不出来问题,但是的需要理解里面的原因所在,我们先看一下结构图:
该图中的Employee继承person类,这是必然的,这就造成了我们应用的时候,一个方法期望的是person对象,那么传递给Employee对象也能工作,此外,personProxy类继承Person类,同样,这也意味着将一个personProxy对象作为参数传递给期望Person对象的方法也是“合法的”。
以同样的方式,EmployeeProxy继承了Employee,意味着你能把一个EmployeeProxy对象作为参数传递给任何期望Employee对象的方法。最后,由于EmployeeProxy继承Employee,Employee又继承Person,这表明EmployeeProxy继承Person,这也就行形成了一个环形继承网(虽然是间接的),从而,任何期望Person对象的方法都可以接受EmployeeProxy对象。
但是上面所描述的问题,代理了一个好处:当我们让抽象工厂开始返回代理对象而不是领域模型类的简单实例的时候,不必再重新考虑如何处理我们的类型层次,我们的客户端依然能够顺利的找到所需要的实例。
简单点讲:如果我们原来向一个期望Person对象的方法传递Employee对象,当我们忽然换成给他们传递EmployeeProxy对象的时候,我们的代码仍然编译通过,而且能够正常运行,原因就是,由于EmployeeProxy继承Employee,并且Employee继承Person,所以没问题。
虽然这种方式看起来无可挑剔,但是有时候我们自己需要注意,比如有一个这样的方法,它接受两个对象,并用反射来确定其中一种对象是否是另一种对象的子类。当我们把一个person对象和一个Employee对象传递给该方法时,它会返回true。但是我们把一个PersonProxy对象和一个EmployeeProxy对象传递给该方法是,突然他就返回false了。