论坛首页 Java企业应用论坛

Domain Model 探索

浏览 104088 次
该帖已经被评为精华帖
作者 正文
   发表时间:2004-12-21  
一直想系统的整理一下自己有关Domain Model实践的尝试。但总觉得自己的想法还不够系统而作罢。
然而从另一方面看“系统的东西”也许永远做不到,失去了目标的生活该会多乏味。
因此我决定将自己有关Domain Model设计的有关实践和思考和盘托出,也算是抛砖引玉。欢迎大家
参与讨论,遇到同你的观点相左的地方,希望能以包容的态度来面对,我们是朝同一方向走的伙伴而不是
相互对视的敌人。:)

在深入讨论之前我先抛出一些原则和概念,最后你会看到这些概念和原则的威力。
1.按照概念依赖的原则来组织业务层。
2.将业务活动(业务流程)建模成类。
3.用业务活动(业务流程)作为关联整个业务层各种对象的骨架。
4.在业务活动中凿出扩展点,使用不同接口分离不同性质业务对象。
5.将对象的存储理解为业务层概念。
......

概念依赖

这是我认为能否得到良好业务层最重要的概念。
在我系统框架设计将要完成,开始涉及业务层设计时,我脑袋一片空白,书上,大家讨论的大多是整个系统的结构从UI层
到服务层到数据访问层到数据库。到底业务层该如何组织?Martin Fowler的POEAA的书中没有回答。找到的相关
书籍也都过于空泛。Martin Fowler的分析模式有些用处,但不够系统。透过Martin fowler网站,我拿到了
Domain Driven Design的发行前版本。该书给了我很大的启示。其中的要点有:
关于关联:
1.Imposing a traversal direction (强制一个关联的导航方向)
......
关于Responsibility Layers(业务职责层)的划分:
作者给出了三个指导原则:Conceptual dependency.(概念依赖)为其中一项。
书中给出的描述的是业务职责层上层的对象需要通过下层对象才能在概念上完整,
相反下层对象则可独立于上层对象存在含义。这样天然的下层对象相对于上层对象
会更稳定。并且在今后演变的过程中,使同扩展的方式来完善系统,而不是改变对象
的方式。
通过实践,我觉得这条原则可以应用在任何两个有关联的业务对象上。通常可以通过
概念依赖先建立一个导航方向。这能够满足大多数的需求。当确实需要反向导航时,
只要理由充分可以随时加上,并且如果先前将这两个对象放入不同包中,这时需要
将他们合并到同一个包中。
我见过一个不好的设计。Customer具有很多Flag分别标记该客户是否挂失,冻结,注销等等。
通常叫做客户状态,然而这是不对的,这违背了单一职责原则。事实上除了注销外
挂失和冻结都不应该算作Customer的本质属性。相反我把他们看作某种约束,进而把挂失看作
一种协议.....因为Customer的概念可以不依赖于挂失和冻结的概念,相反挂失和冻结却要依赖
Customer的概念,应为这是他们动作的主体。
同样的一开始就让Customer有GetAccount的方法同样不好。因为Customer的概念确实不依赖Account
XXXAccount却可以有Customer的属性,Account在概念上依赖Customer。

(待续)
   发表时间:2004-12-21  
按照概念依赖原则我们能更好的理解业务职责层的划分。DDD中建议了如下的职责层。
按从高到低分别为:

依赖方向
| Decision
| Policy
| Commitment
| Operation
V Potential

Potential中包括类似Customer,Employee等Party方面的类。对应支持业务。
Operation中包括了核心业务如存取款,买卖以及同这些业务关联的Account,Product等等。
Commmitment对于客户包括同客户签订的协议等。对于员工来说包括授权等。
Policy包括计算某种收费的策略,比如电信收费的算法。对应支持业务。
Decision包括主要对应于管理业务。监控系统多在这层。
从上到下观察,很好的遵循了概念依赖的原则。
从另一方面来看,可以根据概念随着时间发展的顺序来建立对象之间的关系。这样会天然的满足概念依赖原则。
后面发展起来的概念可以依赖前面的已经存在的概念,而反过来这不可。这是系统稳定的关键。
同客户签订的各种协议可以不断的发展,但是客户对象是稳定的。
同理收费的策略可以变化但是最终反映到帐户上的都只是对Balance的变更。所以收费策略比
帐户更不稳定。
客户对象也要比帐户对象稳定。

从按照稳定的方向依赖的原则出发,我们可以得到对象间的单向依赖。当然也会存在双向关联
的对象,然而这种情况在我的实践中不是很多。而且一旦你懂得了单向关联的好处后,你就会
谨慎的使用双向关联。滥用关联会使得整个业务层象DDD中说的,变成一大块“果冻”,你随便触动
果冻某一块,整个果冻都会颤动。
同样为了简化设计,对象的关系中多对多的关系尽量避免。如果可以
则通过限定角色转化为一对多或一对一的关系。

(待续)
0 请登录后投票
   发表时间:2004-12-21  
关注~~
能不能多举点例子来讲?象下面这段...
便于理解,谢谢!

引用
我见过一个不好的设计。Customer具有很多Flag分别标记该客户是否挂失,冻结,注销等等。
通常叫做客户状态,然而这是不对的,这违背了单一职责原则。事实上除了注销外
挂失和冻结都不应该算作Customer的本质属性。相反我把他们看作某种约束,进而把挂失看作
一种协议.....因为Customer的概念可以不依赖于挂失和冻结的概念,相反挂失和冻结却要依赖
Customer的概念,应为这是他们动作的主体。
0 请登录后投票
   发表时间:2004-12-21  
以上是关于概念依赖的观念,下面让我们看看如何建模业务中的活动。
有一种做法是使用分析模型中的控制类直接映射到设计中类中。我看来这不是好的做法。
这里谈谈分析与设计的区别。
从分析的角度来看,业务实体总是被动的。业务是通过控制对象操作业务实体来完成的。
分析时我们是关注是什么问题。这要求我们客观的来描述现实。
进入设计阶段我们关注的是如何解决的问题。控制对象施加与业务实体的操作加入不涉及
第三者,则这个操作可以并入被操作的实体类中。然而分析中控制对象的概念是如此的
深刻,以至于只涉及Customer的ChangePassword方法该放到哪里都成了问题。类不是
“某概念 + 所关心该概念的属性 + 最终施加与这些属性上的操作” 的封装,又是什么呢?
下面的问题是如何建模跨越多个业务实体的操作?
举个例子:银行开户。
现在假设开户时涉及到下面的一些操作对象。
创建一个Customer对象。
创建一个CapitalAccount对象。
存入一定数额的现金。
记录一笔开户流水。
整个业务活动,我可以建模为OpenCustomerAct对象。伪码如下:

public class OpenCustomerAct extends CustomerAct
{
...
public void override doRun()
{
Customer customer = Customer.create(...);
CapitalAccount capitalAccount = CapitalAccount.create(customer,...);
capitalAccount.deposit(...);
OpenCustomerLog.create(this);
}
...
}

所需的参数通过构造函数得到。
将所有的业务活动都建模成一个Act,这非常重要。甚至你可以在Session中放入一个Act来
表示当前正在进行的业务。所有的扩展都是从Act开始的。
假如你要对Act施加某种检查,那么对doRun方法进行拦截可以达到该目的。
用例能够简化到只剩下流程,同样道理Act也可以做到这点。
对于象RichClient的交互模式,通常只在最后才提交业务,中间的交互都是在准备提交的数据。
那么在中间调用的方法中可以只new XXXAct而不执行doRun操作。这样做是因为中间的调用
可能会用到XXXAct来作为上下文。现在我还没有想好在这样的中间过程中,如何能够触发
植入到donRun前的检查?或许可以创建一个空doRun的子类覆盖掉父类实际的操作?

(待续)
0 请登录后投票
   发表时间:2004-12-21  
强烈关注中。。。。。。

贴完这些概念后,希望能有些实际得例子加以说明。

最近正在做一个项目,业务比较复杂,过几天会总结一下,将需求和设计重写出来。希望能结合着这些东西,让大家讨论一下。
0 请登录后投票
   发表时间:2004-12-22  
Act

public interface Act
{
Operator getOperator();//谁
Date getOccurDate();//在什么时间
String getOccurPlace();//什么地点
BusinessType getBusinessType();//做什么业务
  ActState getActState();//业务运行的当前状态
}
“谁在什么时间什么地点做了什么业务。”
这描述了任何业务的基本方面。从哲学的角度来看,“我们得到了Act,我们就得到了事物的基础”。
当我们具体的描述某项业务时,假如需要向调用方暴露特定的属性。
我们可以随时添加到Act的子接口中。
例如同Customer相关的Act可定义为:
public interface CustomerAct extends Act
{
Cutomer getCustomer();//针对哪个客户
}
在复杂一点的情况下,如业务需要多人协作完成,可以通过组合模式达到目的。

public interface CompositeAct extends Act
{
Act[] getActs();
}
涉及到一段时间有中间状态的工作流也应该可以作为Act的子接口进行扩展。
不过我没有做过这方面的尝试。

将Act放入Session

将Act放入Session使得可以方便得到业务运行的上下文。而且通过扩展Act。
可以从Act或其子接口中得到想得到的任何东西,这使得任何扩展都成为可能。

这里说明一下Act类的位置应当放入Potential层中,并且与Operator在一起。
因为Potential层的业务对象也需要业务活动来维护。
如果你的框架中Sesion在更基础的包中,则可以给Act提供一个空内容的父接口,放入Session所在的包中。
public interface AbstractAct
{
}

public interface Act extends AbstractAct
{
...
}
Session提供得到AbstractAct的入口。
public class Session
{
...
static public AbstractAct getAbstractAct()
{
return Instance().abstractAct;
}
...
}

(待续)
0 请登录后投票
   发表时间:2004-12-23  
Act上的扩展点

按照分层的观点,下层不允许依赖上层,然而业务对象却是协作完成某个目的的。
而且只要业务对象需要维护,就需要相关的Act。
例如:银行中的存钱业务,参考上面的分层,我们把它放入Operation层。
在存钱的业务中,我们需要检查该客户是否做了挂失。而挂失协议我们是放在Commitment层。
显然,Operation层不能直接调用Commitment层的协议。
DIP模式发话了“用我”。
在Operation层中定义Commitment层接口,和一个工厂,使用反射实现这种调用。在Act中调用。
abstract public class ActImpl
extends abstractActImpl
implements Act
{
public virtual void run()
{
doPreprocess();
doRun();
doPostprocess();
}
abstract public  doPreprocess();
abstract public  doRun();
abstract public  doPostprocess();
}

public interface CustomerCommitment
{
void affirmCanDo();
}

abstract public class CustomerActImpl
extends ActImpl
implements CustomerAct
{
...
public override void doPreprocess()
{
...
//扩展点
CustomerCommitment customerCommitment = CustomerCommitmentFactory.create(this);
customerCommitment.affirmCanDo();
...
}
...
}

public interface InnerCustomerCommitment
{
void affirmCanDo(CustomerAct customerAct);
}

public class CustomerCommitmentImpl implements CustomerCommitment
{
private CustomerAct customerAct;

public CustomerCommitmentImpl(CustomerAct customerAct)
{
this.customerAct = customerAct;
}

public void affirmCanDo()
{
...
//通过配置得到该customerAct对应需要检查的客户约束,包括协议,逐一检查。
DomainObjectCollection commitmentTypes = CustomerCommimentRepository.findByBusinessType(customerAct.getBusinessType());

...
foreach( CommitmentType typeItem in commitmentTypes )
{
InnerCustomerCommitment commitment = getCommitment(typeItem);
commitmentItem.affirmCanDo(customerAct);
}
...
}
}

public class CustomerLostReportAgreementChecker implements InnerCustomerCommitment
{
public void affirmCanDo(CustomerAct customerAct)
{
Check.require(customerAct.getCustomer() != null,"客户不存在");

CustomerLostReportAgreement customerLostReportAgreement =
CustomerLostReportAgreementRepository.find(customerAct.getCustomer());

if(customerLostReportAgreement != null)
{
agreement.affirmCanDo(customerAct);
}

}
}

public class CustomerLostReportAgreement
{
...
public void AffirmCanDo(CustomerAct customerAct)
{
if(customerAct.getOccurDate <= expiringDate)
throw new CustomerLossReportedException(customer);
}
...
}

同样道理,可以对其他上层的对象使用DIP使依赖倒置。
比如:电信计算费用。就可以通过在CustomerAct的doRun中插入扩展点来实现。
这样复杂的计费算法就被封装在接口之后了。可以分配另外的人员来开发。
业务活动的流程仍然清晰可见。
是啊,这正是接口的威力,大多数的设计模式不也是基于这种原理吗?

还有在Act上的扩展点可以分为两类,显式的和隐式的。
电信费用的计算就是显式的,因为CustomerAct需要知道计算的结果,用来从帐户中扣除金额。
而检查挂失协议是隐式的,CustomerAct可以对此一无所知。

通过在Act上的扩展点,我们可以向上扩展。
这仿佛是在树枝上种木耳,呵呵。

(待续)
0 请登录后投票
   发表时间:2004-12-23  
总算看到 partech 老大出手了,出手果然不凡呀。

之前承蒙指点,已经获益非浅。现在这里整理出来的东西将有助于从“怎么做”上升到了解“为什么这么做”。

谢谢分享。

希望能尽快看到后面的内容。
0 请登录后投票
   发表时间:2004-12-23  
返濮归真。
0 请登录后投票
   发表时间:2004-12-24  
DIP VS Facade

对于上面的情况,另外一种方法是使用Facade。
让我们比较一下两者。
简要说明一下Facade的做法:
abstract public class CustomerActImpl
extends ActImpl
implements CustomerAct
{
...
public override void doPreprocess()
{
...
//注意:这里传递的参数,会使得用Facade方式的人大伤脑筋。
//按照挂失的要求目前传递getBusinessType(),getCustomer(),getOccurDate()就够了
//但是对于所有的CustomerCommitment这些参数就不一定够了。
//比如:客户可能签订指定员工协议。(指只允许协议中指明的员工能操作的业务)
//那么该接口需要添加getOperator()参数。
//接口变得不稳定。
CustomerCommitmentManager.affirmCanDo(getBusinessType(),getCustomer(),getOccurDate(),?,...);
...
}
...
}

Facade可以使得在Act中也是只提供一个调用点,但是因为不是依赖倒置的关系,不得不显示的说明需要用到的参数。
相反使用DIP模式,接口中定义的是Act的接口,而Act是可以扩展的。(是否扩展全部看上层的对象是否需要)。
而正是因为相应的CustomerCommitment总是处于需要检查的XXXAct的上层。这样具体的CustomerCommitment
总是可以依赖XXXAct。因此可以获得任何想要得到的信息。

同样对于电信计算费用的例子,因为传递的参数是CustomerAct接口。所以对于今后任何可能的扩展该接口都是不会变化的。
能够做到这一点,完全要归功于将计算费用放入Operation的上层Policy中,你能体会到其中的要领吗?

形象一点来说,使用DIP模式,采取的是一种专家模式。
DIP的Act说的是:“CustomerCommitment你看看我现在的情况,还能运行吗?”
相反Facade模式,则是令人厌烦的唠叨模式。
Facade的Act说的是:“CustomerCommitment,现在执行的客户是XXX,业务是XXX,时间是XXX,...你能告诉我还能运行下去吗?”
显然DIP要潇洒得多。

(待续)
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics