1、在深入讨论之前我先抛出一些原则和概念,最后你会看到这些概念和原则的威力。 1.按照概念依赖的原则来组织业务层。 2.将业务活动(业务流程)建模成类。 3.用业务活动(业务流程)作为关联整个业务层各种对象的骨架。 4.在业务活动中凿出扩展点,使用不同接口分离不同性质业务对象。 5.将对象的存储理解为业务层概念。 概念依赖 这是我认为能否得到良好业务层最重要的概念。 在我系统框架设计将要完成,开始涉及业务层设计时,我脑袋一片空白,书上,大家讨论的大多是整个系统的结构从 UI 层 到服务层到数据访问层到数据库。到底业务层该如何组织?Martin Fowler 的 POEAA 的书中没有回答。找到的相
2、关 书籍也都过于空泛。Martin Fowler 的分析模式有些用处,但不够系统。透过 Martin fowler网站,我拿到了 Domain Driven Design 的发行前版本。该书给了我很大的启示。其中的要点有: 关于关联: 1.Imposing a traversal direction (强制一个关联的导航方向) 关于 Responsibility Layers(业务职责层)的划分: 作者给出了三个指导原则:Conceptual dependency.(概念依赖)为其中一项。 书中给出的描述的是业务职责层上层的对象需要通过下层对象才能在概念上完整, 相反下层对象则可独立于上层对象
3、存在含义。这样天然的下层对象相对于上层对象 会更稳定。并且在今后演变的过程中,使同扩展的方式来完善系统,而不是改变对象 的方式。 通过实践,我觉得这条原则可以应用在任何两个有关联的业务对象上。通常可以通过 概念依赖先建立一个导航方向。这能够满足大多数的需求。当确实需要反向导航时, 只要理由充分可以随时加上,并且如果先前将这两个对象放入不同包中,这时需要 将他们合并到同一个包中。 我见过一个不好的设计。Customer 具有很多 Flag 分别标记该客户是否挂失,冻结,注销等等。 通常叫做客户状态,然而这是不对的,这违背了单一职责原则。事实上除了注销外 挂失和冻结都不应该算作 Customer
4、的本质属性。相反我把他们看作某种约束,进而把挂失看作 一种协议.因为 Customer 的概念可以不依赖于挂失和冻结的概念,相反挂失和冻结却要依赖 Customer 的概念,应为这是他们动作的主体。 同样的一开始就让 Customer 有 GetAccount 的方法同样不好。因为 Customer 的概念确实不依赖 Account XXXAccount 却可以有 Customer 的属性,Account 在概念上依赖 Customer。按照概念依赖原则我们能更好的理解业务职责层的划分。DDD 中建议了如下的职责层。 按从高到低分别为: 依赖方向 | Decision | Policy | C
5、ommitment | Operation V Potential Potential 中包括类似 Customer,Employee 等 Party 方面的类。对应支持业务。 Operation 中包括了核心业务如存取款,买卖以及同这些业务关联的 Account,Product 等等。Commmitment 对于客户包括同客户签订的协议等。对于员工来说包括授权等。 Policy 包括计算某种收费的策略,比如电信收费的算法。对应支持业务。 Decision 包括主要对应于管理业务。监控系统多在这层。 从上到下观察,很好的遵循了概念依赖的原则。 从另一方面来看,可以根据概念随着时间发展的顺序来建
6、立对象之间的关系。这样会天然的满足概念依赖原则。 后面发展起来的概念可以依赖前面的已经存在的概念,而反过来这不可。这是系统稳定的关键。 同客户签订的各种协议可以不断的发展,但是客户对象是稳定的。 同理收费的策略可以变化但是最终反映到帐户上的都只是对 Balance 的变更。所以收费策略比 帐户更不稳定。 客户对象也要比帐户对象稳定。 从按照稳定的方向依赖的原则出发,我们可以得到对象间的单向依赖。当然也会存在双向关联 的对象,然而这种情况在我的实践中不是很多。而且一旦你懂得了单向关联的好处后,你就会 谨慎的使用双向关联。滥用关联会使得整个业务层象 DDD 中说的,变成一大块“ 果冻”,你随便触动
7、 果冻某一块,整个果冻都会颤动。 同样为了简化设计,对象的关系中多对多的关系尽量避免。如果可以 则通过限定角色转化为一对多或一对一的关系。以上是关于概念依赖的观念,下面让我们看看如何建模业务中的活动。 有一种做法是使用分析模型中的控制类直接映射到设计中类中。我看来这不是好的做法。 这里谈谈分析与设计的区别。 从分析的角度来看,业务实体总是被动的。业务是通过控制对象操作业务实体来完成的。 分析时我们是关注是什么问题。这要求我们客观的来描述现实。 进入设计阶段我们关注的是如何解决的问题。控制对象施加与业务实体的操作加入不涉及 第三者,则这个操作可以并入被操作的实体类中。然而分析中控制对象的概念是如
8、此的 深刻,以至于只涉及 Customer 的 ChangePassword 方法该放到哪里都成了问题。类不是 “某概念 + 所关心该概念的属性 + 最终施加与这些属性上的操作” 的封装,又是什么呢? 下面的问题是如何建模跨越多个业务实体的操作? 举个例子:银行开户。 现在假设开户时涉及到下面的一些操作对象。 创建一个 Customer 对象。 创建一个 CapitalAccount 对象。 存入一定数额的现金。 记录一笔开户流水。 整个业务活动,我可以建模为 OpenCustomerAct 对象。伪码如下: public class OpenCustomerAct extends Custo
9、merAct . public void override doRun() Customer customer = Customer.create(.); CapitalAccount capitalAccount = CapitalAccount.create(customer,.); capitalAccount.deposit(.); OpenCustomerLog.create(this); . 所需的参数通过构造函数得到。 将所有的业务活动都建模成一个 Act,这非常重要。甚至你可以在 Session 中放入一个 Act来 表示当前正在进行的业务。所有的扩展都是从 Act 开始的。
10、假如你要对 Act 施加某种检查,那么对 doRun 方法进行拦截可以达到该目的。 用例能够简化到只剩下流程,同样道理 Act 也可以做到这点。 对于象 RichClient 的交互模式,通常只在最后才提交业务,中间的交互都是在准备提交的数据。 那么在中间调用的方法中可以只 new XXXAct 而不执行 doRun 操作。这样做是因为中间的调用 可能会用到 XXXAct 来作为上下文。现在我还没有想好在这样的中间过程中,如何能够触发 植入到 donRun 前的检查?或许可以创建一个空 doRun 的子类覆盖掉父类实际的操作?Act public interface Act Operator
11、getOperator();/谁 Date getOccurDate();/在什么时间 String getOccurPlace();/什么地点 BusinessType getBusinessType();/做什么业务 ActState getActState();/业务运行的当前状态 “谁在什么时间什么地点做了什么业务。 ” 这描述了任何业务的基本方面。从哲学的角度来看, “我们得到了 Act,我们就得到了事物的基础” 。 当我们具体的描述某项业务时,假如需要向调用方暴露特定的属性。 我们可以随时添加到 Act 的子接口中。 例如同 Customer 相关的 Act 可定义为: publi
12、c interface CustomerAct extends Act Cutomer getCustomer();/针对哪个客户 在复杂一点的情况下,如业务需要多人协作完成,可以通过组合模式达到目的。 public interface CompositeAct extends Act Act getActs(); 涉及到一段时间有中间状态的工作流也应该可以作为 Act 的子接口进行扩展。 不过我没有做过这方面的尝试。 将 Act 放入 Session 将 Act 放入 Session 使得可以方便得到业务运行的上下文。而且通过扩展 Act。 可以从 Act 或其子接口中得到想得到的任何东西,
13、这使得任何扩展都成为可能。 这里说明一下 Act 类的位置应当放入 Potential 层中,并且与 Operator 在一起。 因为 Potential 层的业务对象也需要业务活动来维护。 如果你的框架中 Sesion 在更基础的包中,则可以给 Act 提供一个空内容的父接口,放入Session 所在的包中。 public interface AbstractAct public interface Act extends AbstractAct . Session 提供得到 AbstractAct 的入口。 public class Session . static public Abst
14、ractAct getAbstractAct() return Instance().abstractAct; . Act 上的扩展点 按照分层的观点,下层不允许依赖上层,然而业务对象却是协作完成某个目的的。 而且只要业务对象需要维护,就需要相关的 Act。 例如:银行中的存钱业务,参考上面的分层,我们把它放入 Operation 层。 在存钱的业务中,我们需要检查该客户是否做了挂失。而挂失协议我们是放在Commitment 层。 显然,Operation 层不能直接调用 Commitment 层的协议。 DIP 模式发话了“ 用我”。 在 Operation 层中定义 Commitment
15、层接口,和一个工厂,使用反射实现这种调用。在 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 a
16、ffirmCanDo(); abstract public class CustomerActImpl extends ActImpl implements CustomerAct . public override void doPreprocess() . /扩展点 CustomerCommitment customerCommitment = CustomerCommitmentFactory.create(this); customerCommitment.affirmCanDo(); . . public interface InnerCustomerCommitment void
17、affirmCanDo(CustomerAct customerAct); public class CustomerCommitmentImpl implements CustomerCommitment private CustomerAct customerAct; public CustomerCommitmentImpl(CustomerAct customerAct) this.customerAct = customerAct; public void affirmCanDo() . /通过配置得到该 customerAct 对应需要检查的客户约束,包括协议,逐一检查。 Doma
18、inObjectCollection commitmentTypes = CustomerCommimentRepository.findByBusinessType(customerAct.getBusinessType(); . foreach( CommitmentType typeItem in commitmentTypes ) InnerCustomerCommitment commitment = getCommitment(typeItem); commitmentItem.affirmCanDo(customerAct); . public class CustomerLos
19、tReportAgreementChecker implements InnerCustomerCommitment public void affirmCanDo(CustomerAct customerAct) Check.require(customerAct.getCustomer() != null,“客户不存在“); CustomerLostReportAgreement customerLostReportAgreement = CustomerLostReportAgreementRepository.find(customerAct.getCustomer(); if(cus
20、tomerLostReportAgreement != null) agreement.affirmCanDo(customerAct); public class CustomerLostReportAgreement . public void AffirmCanDo(CustomerAct customerAct) if(customerAct.getOccurDate = expiringDate) throw new CustomerLossReportedException(customer); . 同样道理,可以对其他上层的对象使用 DIP 使依赖倒置。 比如:电信计算费用。就可
21、以通过在 CustomerAct 的 doRun 中插入扩展点来实现。 这样复杂的计费算法就被封装在接口之后了。可以分配另外的人员来开发。 业务活动的流程仍然清晰可见。 是啊,这正是接口的威力,大多数的设计模式不也是基于这种原理吗? 还有在 Act 上的扩展点可以分为两类,显式的和隐式的。 电信费用的计算就是显式的,因为 CustomerAct 需要知道计算的结果,用来从帐户中扣除金额。 而检查挂失协议是隐式的,CustomerAct 可以对此一无所知。 通过在 Act 上的扩展点,我们可以向上扩展。 这仿佛是在树枝上种木耳,呵呵。DIP VS Facade 对于上面的情况,另外一种方法是使用
22、 Facade。 让我们比较一下两者。 简要说明一下 Facade 的做法: abstract public class CustomerActImpl extends ActImpl implements CustomerAct . public override void doPreprocess() . /注意:这里传递的参数,会使得用 Facade 方式的人大伤脑筋。 /按照挂失的要求目前传递 getBusinessType(),getCustomer(),getOccurDate()就够了 /但是对于所有的 CustomerCommitment 这些参数就不一定够了。 /比如:客户可
23、能签订指定员工协议。 (指只允许协议中指明的员工能操作的业务) /那么该接口需要添加 getOperator()参数。 /接口变得不稳定。 CustomerCommitmentManager.affirmCanDo(getBusinessType(),getCustomer(),getOccurDate(),?,.); . . Facade 可以使得在 Act 中也是只提供一个调用点,但是因为不是依赖倒置的关系,不得不显示的说明需要用到的参数。 相反使用 DIP 模式,接口中定义的是 Act 的接口,而 Act 是可以扩展的。( 是否扩展全部看上层的对象是否需要)。 而正是因为相应的 Cust
24、omerCommitment 总是处于需要检查的 XXXAct 的上层。这样具体的CustomerCommitment 总是可以依赖 XXXAct。因此可以获得任何想要得到的信息。 同样对于电信计算费用的例子,因为传递的参数是 CustomerAct 接口。所以对于今后任何可能的扩展该接口都是不会变化的。 能够做到这一点,完全要归功于将计算费用放入 Operation 的上层 Policy 中,你能体会到其中的要领吗? 形象一点来说,使用 DIP 模式,采取的是一种专家模式。 DIP 的 Act 说的是:“CustomerCommitment 你看看我现在的情况,还能运行吗?” 相反 Faca
25、de 模式,则是令人厌烦的唠叨模式。 Facade 的 Act 说的是:“CustomerCommitment,现在执行的客户是 XXX,业务是 XXX,时间是XXX,.你能告诉我还能运行下去吗?” 显然 DIP 要潇洒得多。实现接口 VS 继承父类 这里稍稍偏离一下主题,讨论一下接口同继承的问题。 什么时候使用接口?什么时候使用继承? 这似乎是个感觉和经验问题。或者我们会倾向于多使用接口,少使用继承。 可不可以再进一步呢? 以下是我的观点: “接口是调用方要求的结果,而继承则是实现方思考的产物。 ” 毕竟如果我们定义的接口没有被用到,那它就没有任何用处。 接口的目的在于制定虚的标准,从而使调
26、用方不依赖于实现方。 而继承某个父类则多半是基于“偷懒“的考虑,已经存在的东西,我为什么不利用一下? 当然这样说是忽略了继承的真正用意单点维护。 所以在定义 XXXAct 的接口时,需要多考虑一下,上层对象需要 Act 中的提供什么特性,会如何使用它。 接口属于调用方。业务对象的持久化 一个会引起争议的问题,是业务层是否会涉及业务对象持久化的概念。 答案是肯定的。 DDD 中在描述 The life cycle of a domain object 时,给出了两种形式的持久化。 Store 和 Archive。我们使用的较多是 Store。 但是这不代表业务层要依赖数据访问层。相反依赖关系应该
27、倒过来。数据访问层依赖 业务层。通常我们使用 Mapper 实现,在 hibernate 中通过配置达到该目的。 要做到业务层不依赖于数据访问层,同样借助接口来完成。 在业务层定义数据访问的接口,为了方便,可以使用一个类来封装这些操作。 public interface CustomerFinder Customer findByID(ID id); Customer findByCode(String code); DomainObjectCollection findByName(String name); . public class CustomerRepository private
28、 static CustomerFinder finder = null; private static CustomerFinder getFinderInstance() if (finder = null) finder = (CustomerFinder)FinderRegistry.getFinder(“CustomerFinder“); return finder; public static Customer findByID(ID id) Customer obj = getFinderInstance().findByID(id); Check.require(obj !=
29、null, “未找到 ID 为: “ + id.toString() + “对应的 Customer。“); return obj; . 在数据访问层实现这些接口。因为是数据访问层依赖业务层,所以你可以采用多种技术来实现, 使用 hibernate 这样的开源项目,或者手工编写 Mapper。 ID id 另外一个有争议的问题是 Domain 层是否要引入与业务无关的 ID 来标识不同的对象呢? 我的经验是在业务层引入 ID 的概念会使很多事情变得方便些。 如:Lazyload。 这是否不属于业务的范畴?是在概念上不属于业务。但在业务上 不是没有对应的概念。 例如:保存客户定购信息的订单,作为
30、标识的就是订单号,这是给人使用的。 在使用电脑后,我们也给对象一个它能理解的统一标识,这就是 ID。 另外不要使用业务上的概念作为主键和外键,因为它们本来就不是数据库的概念。 否则,会使得业务概念同数据库的概念混淆起来。 ID 的使用通常会选择效率较高的 long 类型。 不过我们的实现走得更远,我们将其封装为 ID 对象。Service 层 现在我们向上看看将业务层包裹的服务层。 服务层是架设在应用层和业务层的桥梁,用来封装对业务层的访问,因此 可以把服务层看作中介,充当两个角色: 1.实现应用层接口要求的接口; 2.作为业务层的外观。 服务层的典型调用如下: public interfac
31、e 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
32、 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 custome
33、rLostReport(String customerCode,Date expiringDate,String remark) try Check.require(customerCode != null init(); CustomerLostReportAct customerLostReportAct = new CustomerLostReportAct(customerCode, expiringDate, remark); customerLostReportAct.run(); commit(); catch(Exception e) throw ExceptionPostpr
34、ocess(e); public CutomerBasicInfo getCutomerBasicInfo(String customerCode) try Check.require(customerCode != null init(); Customer customer = CustomerRepository.findByCode(customerCode); /这里选择的是在 CustomerRepository 外抛出 CustomerNotFoundException 异常, /另一种方法是在 CustomerRepository 中抛出 CustomerNotFoundExc
35、eption 异常。 /因为 CustomerRepository 在于通过客户代码查找对应的客户。至于是否应该抛出 /异常则交给业务层或服务层来处理。 /这里有很微妙的区别,抛出 CustomerNotFoundException 应该是谁的职责呢? /你的想法是什么?:) if(customer = null) throw new CustomerNotFoundException(customerCode); CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer); return cut
36、omerBasicInfo; catch(Exception e) throw ExceptionPostprocess(e); . 服务层的代码很简单,不是吗? 上面的代码可以通过 AOP 进一步的简化。使用 AOP 实现我希望代码象下面这样简单。 public class CustomerServicesImpl implements CustomerServices . public void openCustomer(CustomerInfo cutomerInfo) OpenCustomerAct openCustomerAct = new OpenCustomerAct(custo
37、merInfo.name, customerInfo.code, customerInfo.address, customerInfo.plainpassword . ); openCustomerAct.run(); public void customerLostReport(String customerCode,Date expiringDate,String remark) Check.require(customerCode != null CustomerLostReportAct customerLostReportAct = new CustomerLostReportAct
38、(customerCode, expiringDate, remark); customerLostReportAct.run(); public CutomerBasicInfo getCutomerBasicInfo(String customerCode) Customer customer = CustomerRepository.findByCode(customerCode); if(customer = null) throw new CustomerNotFoundException(customerCode); CutomerBasicInfo cutomerBasicInf
39、o = CutomerBasicInfoAssembler.create(customer); return cutomerBasicInfo; DTO or Not 我认为是否使用 DTO 取决于项目的大小,开发团队的结构,以及对项目演变预期的评估结果。不使用 DTO 而直接使用 PO 传递到应用层适用于一个人同时负责应用层和业务层的短期简单项目; 一旦采用该模式作为构架,我不知道业务层是否还能叫做面向对象。 原因如下: 1.使用 PO 承担 DTO 的职责传递到应用层,迫使 PO 不能包含业务逻辑,这样业务逻辑会暴露给应用层。 业务逻辑将由类似于 XXXManager 的类承担,这样看来似
40、乎 PO 有了更多的复用机会,因为PO 只包含 getXXX 同 setXXX 类似的属性。 然而这正类似面向过程模式的范例,使用方法操作结构,程序多少又回到了面向过程的方式。 2.将 PO 直接传递到应用层,迫使应用层依赖于业务层,如果一个人同时负责应用层和业务层那么问题不大; 如果是分别由不同的人开发,将使得应用层开发人员必须了解业务层对象结构的细节,增加了应用层开发人员的知识范围。 同时因为这种耦合,开发的并行受到影响,相互交流增多。 3.此外这也会使得业务层在构建 PO 时要特别小心,因为需要考虑传递到应用层效率问题,在构建业务层时需要 考虑应用层的需要解决的问题是不是有些奇怪? 有人会抱怨写 XXXAssember 太麻烦,我的经验是 XXXAssembler 都很简单。 我们使用手机,会发现大多数手机提供给的接口都是相同的,这包括 0-9 的数字键,绿色的接听键,红色的挂机键,还有一块显示屏。 无论我是拿到 NOkIA,还是 MOTO 的手机,我都能使用,作为手机使用者我没有必要知道手机界面下的结构,不用关心 使用的是 SmartPhone 还是 Symbian。 确实, 应用层将服务层和业务层看作黑箱要比看作白箱好得多。