`

Domain Model 探索(转)

阅读更多

一直想系统的整理一下自己有关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。

按照概念依赖原则我们能更好的理解业务职责层的划分。DDD中建议了如下的职责层。
按从高到低分别为:

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

 

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

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

以上是关于概念依赖的观念,下面让我们看看如何建模业务中的活动。
有一种做法是使用分析模型中的控制类直接映射到设计中类中。我看来这不是好的做法。
这里谈谈分析与设计的区别。
从分析的角度来看,业务实体总是被动的。业务是通过控制对象操作业务实体来完成的。
分析时我们是关注是什么问题。这要求我们客观的来描述现实。
进入设计阶段我们关注的是如何解决的问题。控制对象施加与业务实体的操作加入不涉及
第三者,则这个操作可以并入被操作的实体类中。然而分析中控制对象的概念是如此的
深刻,以至于只涉及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的子类覆盖掉父类实际的操作?

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; 
    } 
    ... 
} 
 

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); 
        }
    } 
    ... 
} 

 

在数据访问层实现这些接口。因为是数据访问层依赖业务层,所以你可以采用多种技术来实现,
使用hibernate这样的开源项目,或者手工编写Mapper。

ID id

另外一个有争议的问题是Domain层是否要引入与业务无关的ID来标识不同的对象呢?
我的经验是在业务层引入ID的概念会使很多事情变得方便些。
如:Lazyload。
这是否不属于业务的范畴?是在概念上不属于业务。但在业务上
不是没有对应的概念。
例如:保存客户定购信息的订单,作为标识的就是订单号,这是给人使用的。
在使用电脑后,我们也给对象一个它能理解的统一标识,这就是ID。
另外不要使用业务上的概念作为主键和外键,因为它们本来就不是数据库的概念。
否则,会使得业务概念同数据库的概念混淆起来。

ID的使用通常会选择效率较高的long类型。
不过我们的实现走得更远,我们将其封装为ID对象。

Service层

现在我们向上看看将业务层包裹的服务层。
服务层是架设在应用层和业务层的桥梁,用来封装对业务层的访问,因此
可以把服务层看作中介,充当两个角色:
1.实现应用层接口要求的接口;
2.作为业务层的外观。
服务层的典型调用如下:

public interface CustomerServices { 
    void openCustomer(CustomerInfo cutomerInfo); 
    void customerLostReport(String customerCode,Date expiringDate,String remark); 
    CutomerBasicInfo getCutomerBasicInfo(String customerCode); 
    ... 
} 

public class CustomerServicesImpl extends ServiceFacade implements CustomerServices { 
    ... 
    public void openCustomer(CustomerInfo cutomerInfo) { 
        try { 
            init(); 
            OpenCustomerAct openCustomerAct = new OpenCustomerAct(customerInfo.name, 
            customerInfo.code, customerInfo.address, customerInfo.plainpassword ... ); 
            openCustomerAct.run(); 
            commit(); 
        } 
        catch(Exception e) { 
            throw ExceptionPostprocess(e); 
        } 
    } 

    public void customerLostReport(String customerCode,Date expiringDate,String remark) { 
        try { 
            Check.require(customerCode != null && customerCode != "", "无效的客户代码:" + customerCode); 
            init(); 
            CustomerLostReportAct customerLostReportAct = new CustomerLostReportAct(
                customerCode, expiringDate, remark); 
            customerLostReportAct.run(); 
            commit(); 
        } 
        catch(Exception e) { 
            throw ExceptionPostprocess(e); 
        } 
    } 

    public CutomerBasicInfo getCutomerBasicInfo(String customerCode) { 
        try { 
            Check.require(customerCode != null && customerCode != "", "无效的客户代码:" + customerCode); 
            init(); 
            Customer customer = CustomerRepository.findByCode(customerCode); 
//这里选择的是在CustomerRepository外抛出CustomerNotFoundException异常, 
//另一种方法是在CustomerRepository中抛出CustomerNotFoundException异常。 
//因为CustomerRepository在于通过客户代码查找对应的客户。至于是否应该抛出 
//异常则交给业务层或服务层来处理。 
//这里有很微妙的区别,抛出CustomerNotFoundException应该是谁的职责呢? 
//你的想法是什么?:) 
            if(customer == null) {
                throw new CustomerNotFoundException(customerCode); 
            }
            CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer); 
            return cutomerBasicInfo; 
            } 
            catch(Exception e) { 
                throw ExceptionPostprocess(e); 
            } 
    } 
... 
} 

 

服务层的代码很简单,不是吗?

上面的代码可以通过AOP进一步的简化。使用AOP实现我希望代码象下面这样简单。

public class CustomerServicesImpl implements CustomerServices { 
    ... 
    public void openCustomer(CustomerInfo cutomerInfo) { 
        OpenCustomerAct openCustomerAct = new OpenCustomerAct(customerInfo.name, 
            customerInfo.code, customerInfo.address, customerInfo.plainpassword ... ); 
        openCustomerAct.run(); 
    } 

    public void customerLostReport(String customerCode,Date expiringDate,String remark) { 
        Check.require(customerCode != null && customerCode != "", "无效的客户代码:" + customerCode); 
        CustomerLostReportAct customerLostReportAct = new CustomerLostReportAct(
            customerCode, expiringDate, remark); 
        customerLostReportAct.run(); 
} 

    public CutomerBasicInfo getCutomerBasicInfo(String customerCode) { 
        Customer customer = CustomerRepository.findByCode(customerCode); 
        if(customer == null) {
            throw new CustomerNotFoundException(customerCode); 
        }
        CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer); 
        return cutomerBasicInfo; 
} 

 

DTO or Not

我认为是否使用DTO取决于项目的大小,开发团队的结构,以及对项目演变预期的评估结果。
不使用DTO而直接使用PO传递到应用层适用于一个人同时负责应用层和业务层的短期简单项目;
一旦采用该模式作为构架,我不知道业务层是否还能叫做面向对象。
原因如下:
1.使用PO承担DTO的职责传递到应用层,迫使PO不能包含业务逻辑,这样业务逻辑会暴露给应用层。
业务逻辑将由类似于XXXManager的类承担,这样看来似乎PO有了更多的复用机会,因为PO只包含getXXX同setXXX类似的属性。
然而这正类似面向过程模式的范例,使用方法操作结构,程序多少又回到了面向过程的方式。
2.将PO直接传递到应用层,迫使应用层依赖于业务层,如果一个人同时负责应用层和业务层那么问题不大;
如果是分别由不同的人开发,将使得应用层开发人员必须了解业务层对象结构的细节,增加了应用层开发人员的知识范围。
同时因为这种耦合,开发的并行受到影响,相互交流增多。
3.此外这也会使得业务层在构建PO时要特别小心,因为需要考虑传递到应用层效率问题,在构建业务层时需要
考虑应用层的需要解决的问题是不是有些奇怪?

有人会抱怨写XXXAssember太麻烦,我的经验是XXXAssembler都很简单。

我们使用手机,会发现大多数手机提供给的接口都是相同的,这包括0-9的数字键,绿色的接听键,红色的挂机键,还有一块显示屏。
无论我是拿到NOkIA,还是MOTO的手机,我都能使用,作为手机使用者我没有必要知道手机界面下的结构,不用关心
使用的是SmartPhone还是Symbian。

确实,应用层将服务层和业务层看作黑箱要比看作白箱好得多。

分享到:
评论

相关推荐

    探索DomainModel系列

    MapperRegistry是工厂方法的变形?摘要本文通过由ActiveRecord模式到DataMapper模式(使用工厂方法)再到DataMapper模式(使用MapperRegistry)的一系列重构,探讨模式背后隐藏的思想和面向对象设计原则。...

    领域驱动设计精简版,domain driven design

    在DDD中,我们首先会定义业务领域的核心概念,这些概念被称为领域模型(Domain Model)。领域模型由实体(Entity)、值对象(Value Object)、聚合(Aggregate)、领域事件(Domain Event)等元素组成。实体是有唯一...

    一种基于J2EE 平台的MDA 模型转换技术* (2005年)

    - **Application Model**:作为PSM的一部分,Application Model是在Domain Model的基础上,结合特定的技术框架(本案例中为J2EE)进行细化的结果。它不仅包含了业务逻辑,还明确了如何使用J2EE平台的具体组件和服务...

    领域和类别转移下的UMAD通用模型自适应_UMAD Universal Model Adaptation under Domai

    UMAD(Universal Model Adaptation)框架是针对领域和类别转移问题的一种通用模型自适应方法,主要应用于无监督领域适应(Unsupervised Domain Adaptation, UDA)。UMAD旨在解决两个关键问题:一是处理开放集(open-...

    Model Research of Electric Coal Calorific Value Based on Near Infrared Frequency Domain Self-Adaption Analysis Method

    本研究基于近红外光谱学原理,利用近红外光谱技术对燃煤样品进行光谱采集,并结合频域分析技术,探索一种新的自适应分析方法。该方法能够根据不同的燃煤样品自动调整参数,提高模型的预测精度。 ### 近红外光谱技术...

    Topic detection model in a single-domain corpus inspired by the human memory cognitive process

    因此,开发高效的主题检测模型对于探索科学、技术或社会事件的现状和发展趋势至关重要。传统的主题检测模型如LDA和KeyGraph等虽然提供了一定的基础,但仍然存在局限性,特别是在单领域语料库的应用上。 #### 新模型...

    利用混合生成的假转录物克服低资源序列中的域错配对ASR模型进行测序_Overcoming Domain Mismatch in

    这篇论文"利用混合生成的假转录物克服低资源序列中的域错配对ASR模型进行测序"(Overcoming Domain Mismatch in Low Resource Sequence-to-Sequence ASR Models using Hybrid Generated Pseudotranscripts)探讨了这...

    Ouobpo-开源

    在提供的压缩包文件中,我们看到一个名为`s2-domainmodel`的子文件夹,这通常代表着一个特定的模块或者领域模型组件。在Java开发中,领域模型是业务逻辑的核心部分,它代表了应用程序中的实体、值对象和服务等概念,...

    Android代码-Android-Architecture

    该项目结合 MVP 与 Clean 架构思想,探索在 Android 项目上的最佳实践。 遵循 Clean Architecture 的原则。 数据层(Data Layer):加入数据转换层(Mapper)将服务端数据模型(Entity)与本地数据模型(Model)...

    细说业务逻辑

    - **概述**:Domain Model 强调通过领域实体来表达业务逻辑,支持更复杂的业务场景。 - **分析**:虽然增加了初始开发成本,但长期来看提高了系统的可维护性和扩展性。 #### 3.5、各种架构模式的比较及选择 - **...

    pytorch-adda.zip

    2. **目标域模型(Target Domain Model)**:在训练过程中,这个模型的权重被冻结,只用于目标域样本的特征提取。 3. **生成器(Generator)**:将源域的特征转换为目标域的特征,目的是让判别器无法区分生成的特征...

    Android-AndroidCleanArchitectureMVI样板

    **Android Clean Architecture MVI 模式详解** 在Android应用开发中,保持代码的整洁和可维护性至关重要。Clean Architecture是一种架构模式,旨在提供清晰的层次...不断探索和实践,是提升Android开发技能的关键。

    领域驱动开发

    2. **领域模型**(Domain Model):这是DDD的核心,是对领域知识的抽象表示,包含了业务实体、值对象、聚合、领域事件等元素。它不仅包含数据结构,更重要的是业务逻辑和规则。 3. **实体**(Entity):具有唯一...

    实现领域驱动设计

    2. **领域模型**(Domain Model):是领域知识的抽象表现,用对象和它们之间的关系来表示业务实体、行为和规则。 3. **聚合**(Aggregate):是领域模型中的一组相关对象,它们作为一个整体被修改和维护,聚合根是...

    stargan-my1

    - **多域分类器(Domain Classifier)**: 判断图像属于哪个领域,辅助生成器进行正确的转换。 - **损失函数(Loss Function)**:包括对抗损失、域一致性损失、身份保持损失等,确保生成图像的视觉质量和转换的准确...

    DDD领域驱动设计大会演讲ppt(3/5)

    1. **领域模型(Domain Model)**:领域模型是业务规则和业务逻辑的抽象表现,它由领域实体(Entities)、值对象(Value Objects)、聚合(Aggregates)、领域服务(Domain Services)和工厂(Factories)等组成。...

    基于gef框架Demo

    这个框架提供了一种高效且灵活的方式来开发复杂的图形用户界面,尤其适用于创建和编辑领域特定模型(Domain-Specific Models,DSM)。在“基于gef框架Demo”中,我们很可能会发现一系列用于展示Gef功能的示例程序。 ...

    Nhibernate配置入门.pdf

    - **Nhibernate.DomainModel**:这里是测试Nhibernate的DomainModel类定义和映射文档的所在,包括`.hbm.xml`和对应的`.cs`文件,展示了类和映射定义。 - **Nhibernate.Eg**:包含了一些简单的Nhibernate示例,但它们...

Global site tag (gtag.js) - Google Analytics