Act上的扩展点
按照分层的观点,下层不允许依赖上层,然而业务对象却是协作完成某个目的的。
而且只要业务对象需要维护,就需要相关的Act。
例如:银行中的存钱业务,参考上面的分层,我们把它放入Operation层。
在存钱的业务中,我们需要检查该客户是否做了挂失。而挂失协议我们是放在Commitment层。
显然,Operation层不能直接调用Commitment层的协议。
DIP模式发话了“用我”。
在Operation层中定义Commitment层接口,和一个工厂,使用反射实现这种调用。在Act中调用。
1abstract public class ActImpl
2extends abstractActImpl
3implements Act
4{
5public virtual void run()
6{
7doPreprocess();
8doRun();
9doPostprocess();
10}
11abstract public doPreprocess();
12abstract public doRun();
13abstract public doPostprocess();
14}
15
16public interface CustomerCommitment
17{
18void affirmCanDo();
19}
20
21abstract public class CustomerActImpl
22extends ActImpl
23implements CustomerAct
24{
25
26public override void doPreprocess()
27{
28
29//扩展点
30CustomerCommitment customerCommitment = CustomerCommitmentFactory.create(this);
31customerCommitment.affirmCanDo();
32
33}
34
35}
36
37public interface InnerCustomerCommitment
38{
39void affirmCanDo(CustomerAct customerAct);
40}
41
42public class CustomerCommitmentImpl implements CustomerCommitment
43{
44private CustomerAct customerAct;
45
46public CustomerCommitmentImpl(CustomerAct customerAct)
47{
48this.customerAct = customerAct;
49}
50
51public void affirmCanDo()
52{
53
54//通过配置得到该customerAct对应需要检查的客户约束,包括协议,逐一检查。
55DomainObjectCollection commitmentTypes = CustomerCommimentRepository.findByBusinessType(customerAct.getBusinessType());
56
57
58foreach( CommitmentType typeItem in commitmentTypes )
59{
60InnerCustomerCommitment commitment = getCommitment(typeItem);
61commitmentItem.affirmCanDo(customerAct);
62}
63
64}
65}
66
67public class CustomerLostReportAgreementChecker implements InnerCustomerCommitment
68{
69public void affirmCanDo(CustomerAct customerAct)
70{
71Check.require(customerAct.getCustomer() != null,"客户不存在");
72
73CustomerLostReportAgreement customerLostReportAgreement =
74CustomerLostReportAgreementRepository.find(customerAct.getCustomer());
75
76if(customerLostReportAgreement != null)
77{
78agreement.affirmCanDo(customerAct);
79}
80
81}
82}
83
84public class CustomerLostReportAgreement
85{
86
87public void AffirmCanDo(CustomerAct customerAct)
88{
89if(customerAct.getOccurDate <= expiringDate)
90throw new CustomerLossReportedException(customer);
91}
92
93}
94 同样道理,可以对其他上层的对象使用DIP使依赖倒置。
比如:电信计算费用。就可以通过在CustomerAct的doRun中插入扩展点来实现。
这样复杂的计费算法就被封装在接口之后了。可以分配另外的人员来开发。
业务活动的流程仍然清晰可见。
是啊,这正是接口的威力,大多数的设计模式不也是基于这种原理吗?
还有在Act上的扩展点可以分为两类,显式的和隐式的。
电信费用的计算就是显式的,因为CustomerAct需要知道计算的结果,用来从帐户中扣除金额。
而检查挂失协议是隐式的,CustomerAct可以对此一无所知。
通过在Act上的扩展点,我们可以向上扩展。
这仿佛是在树枝上种木耳,呵呵。
DIP VS Facade
对于上面的情况,另外一种方法是使用Facade。
让我们比较一下两者。
简要说明一下Facade的做法:
1abstract public class CustomerActImpl
2extends ActImpl
3implements CustomerAct
4{
5
6public override void doPreprocess()
7{
8
9//注意:这里传递的参数,会使得用Facade方式的人大伤脑筋。
10//按照挂失的要求目前传递getBusinessType(),getCustomer(),getOccurDate()就够了
11//但是对于所有的CustomerCommitment这些参数就不一定够了。
12//比如:客户可能签订指定员工协议。(指只允许协议中指明的员工能操作的业务)
13//那么该接口需要添加getOperator()参数。
14//接口变得不稳定。
15CustomerCommitmentManager.affirmCanDo(getBusinessType(),getCustomer(),getOccurDate(),?,);
16
17}
18
19}
20 Facade可以使得在Act中也是只提供一个调用点,但是因为不是依赖倒置的关系,不得不显示的说明需要用到的参数。
相反使用DIP模式,接口中定义的是Act的接口,而Act是可以扩展的。(是否扩展全部看上层的对象是否需要)。
而正是因为相应的CustomerCommitment总是处于需要检查的XXXAct的上层。这样具体的CustomerCommitment
总是可以依赖XXXAct。因此可以获得任何想要得到的信息。
同样对于电信计算费用的例子,因为传递的参数是CustomerAct接口。所以对于今后任何可能的扩展该接口都是不会变化的。
能够做到这一点,完全要归功于将计算费用放入Operation的上层Policy中,你能体会到其中的要领吗?
形象一点来说,使用DIP模式,采取的是一种专家模式。
DIP的Act说的是:“CustomerCommitment你看看我现在的情况,还能运行吗?”
相反Facade模式,则是令人厌烦的唠叨模式。
Facade的Act说的是:“CustomerCommitment,现在执行的客户是XXX,业务是XXX,时间是XXX,...你能告诉我还能运行下去吗?”
显然DIP要潇洒得多。
实现接口 VS 继承父类
这里稍稍偏离一下主题,讨论一下接口同继承的问题。
什么时候使用接口?什么时候使用继承?
这似乎是个感觉和经验问题。或者我们会倾向于多使用接口,少使用继承。
可不可以再进一步呢?
以下是我的观点:
“接口是调用方要求的结果,而继承则是实现方思考的产物。”
毕竟如果我们定义的接口没有被用到,那它就没有任何用处。
接口的目的在于制定虚的标准,从而使调用方不依赖于实现方。
而继承某个父类则多半是基于“偷懒“的考虑,已经存在的东西,我为什么不利用一下?
当然这样说是忽略了继承的真正用意--单点维护。
所以在定义XXXAct的接口时,需要多考虑一下,上层对象需要Act中的提供什么特性,会如何使用它。
接口属于调用方。
业务对象的持久化
一个会引起争议的问题,是业务层是否会涉及业务对象持久化的概念。
答案是肯定的。
DDD中在描述The life cycle of a domain object时,给出了两种形式的持久化。
Store和Archive。我们使用的较多是Store。
但是这不代表业务层要依赖数据访问层。相反依赖关系应该倒过来。数据访问层依赖
业务层。通常我们使用Mapper实现,在hibernate中通过配置达到该目的。
要做到业务层不依赖于数据访问层,同样借助接口来完成。
在业务层定义数据访问的接口,为了方便,可以使用一个类来封装这些操作。
1public interface CustomerFinder
2{
3Customer findByID(ID id);
4Customer findByCode(String code);
5DomainObjectCollection findByName(String name);
6
7}
8
9public class CustomerRepository
10{
11private static CustomerFinder finder = null;
12private static CustomerFinder getFinderInstance()
13{
14if (finder == null)
15{
16finder = (CustomerFinder)FinderRegistry.getFinder("CustomerFinder");
17}
18return finder;
19}
20
21public static Customer findByID(ID id)
22{
23Customer obj = getFinderInstance().findByID(id);
24Check.require(obj != null,
25"未找到ID为:" + id.toString() +
26"对应的 Customer。");
27return obj;
28}
29
30}
31 在数据访问层实现这些接口。因为是数据访问层依赖业务层,所以你可以采用多种技术来实现,
使用hibernate这样的开源项目,或者手工编写Mapper。
ID id
另外一个有争议的问题是Domain层是否要引入与业务无关的ID来标识不同的对象呢?
我的经验是在业务层引入ID的概念会使很多事情变得方便些。
如:Lazyload。
这是否不属于业务的范畴?是在概念上不属于业务。但在业务上
不是没有对应的概念。
例如:保存客户定购信息的订单,作为标识的就是订单号,这是给人使用的。
在使用电脑后,我们也给对象一个它能理解的统一标识,这就是ID。
另外不要使用业务上的概念作为主键和外键,因为它们本来就不是数据库的概念。
否则,会使得业务概念同数据库的概念混淆起来。
ID的使用通常会选择效率较高的long类型。
不过我们的实现走得更远,我们将其封装为ID对象。
Service层
现在我们向上看看将业务层包裹的服务层。
服务层是架设在应用层和业务层的桥梁,用来封装对业务层的访问,因此
可以把服务层看作中介,充当两个角色:
1.实现应用层接口要求的接口;
2.作为业务层的外观。
服务层的典型调用如下:
1public interface CustomerServices
2{
3void openCustomer(CustomerInfo cutomerInfo);
4void customerLostReport(String customerCode,Date expiringDate,String remark);
5CutomerBasicInfo getCutomerBasicInfo(String customerCode);
6
7}
8
9public class CustomerServicesImpl
10extends ServiceFacade
11implements CustomerServices
12{
13
14public void openCustomer(CustomerInfo cutomerInfo)
15{
16try
17{
18init();
19
20OpenCustomerAct openCustomerAct =
21new OpenCustomerAct(customerInfo.name,
22customerInfo.code,
23customerInfo.address,
24customerInfo.plainpassword
25
26);
27openCustomerAct.run();
28
29commit();
30}
31catch(Exception e)
32{
33throw ExceptionPostprocess(e);
34}
35}
36
37public void customerLostReport(String customerCode,Date expiringDate,String remark)
38{
39try
40{
41Check.require(customerCode != null && customerCode != "",
42"无效的客户代码:" + customerCode);
43init();
44
45CustomerLostReportAct customerLostReportAct =
46new CustomerLostReportAct(customerCode,
47expiringDate,
48remark);
49customerLostReportAct.run();
50
51commit();
52}
53catch(Exception e)
54{
55throw ExceptionPostprocess(e);
56}
57}
58
59public CutomerBasicInfo getCutomerBasicInfo(String customerCode)
60{
61try
62{
63Check.require(customerCode != null && customerCode != "",
64"无效的客户代码:" + customerCode);
65init();
66Customer customer = CustomerRepository.findByCode(customerCode);
67
68//这里选择的是在CustomerRepository外抛出CustomerNotFoundException异常,
69//另一种方法是在CustomerRepository中抛出CustomerNotFoundException异常。
70//因为CustomerRepository在于通过客户代码查找对应的客户。至于是否应该抛出
71//异常则交给业务层或服务层来处理。
72//这里有很微妙的区别,抛出CustomerNotFoundException应该是谁的职责呢?
73//你的想法是什么?
74if(customer == null)
75throw new CustomerNotFoundException(customerCode);
76
77CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer);
78return cutomerBasicInfo;
79}
80catch(Exception e)
81{
82throw ExceptionPostprocess(e);
83}
84}
85
86
87}
88
89服务层的代码很简单,不是吗?
90
91上面的代码可以通过AOP进一步的简化。使用AOP实现我希望代码象下面这样简单。
92public class CustomerServicesImpl
93implements CustomerServices
94{
95
96public void openCustomer(CustomerInfo cutomerInfo)
97{
98OpenCustomerAct openCustomerAct =
99new OpenCustomerAct(customerInfo.name,
100customerInfo.code,
101customerInfo.address,
102customerInfo.plainpassword
103
104);
105openCustomerAct.run();
106}
107
108public void customerLostReport(String customerCode,Date expiringDate,String remark)
109{
110Check.require(customerCode != null && customerCode != "",
111"无效的客户代码:" + customerCode);
112CustomerLostReportAct customerLostReportAct =
113new CustomerLostReportAct(customerCode,
114expiringDate,
115remark);
116customerLostReportAct.run();
117}
118
119public CutomerBasicInfo getCutomerBasicInfo(String customerCode)
120{
121Customer customer = CustomerRepository.findByCode(customerCode);
122if(customer == null)
123throw new CustomerNotFoundException(customerCode);
124
125CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer);
126return cutomerBasicInfo;
127}
128 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。
确实,应用层将服务层和业务层看作黑箱要比看作白箱好得多。
posted on 2005-09-29 00:41
老妖 阅读(399)
评论(0) 编辑 收藏