Act上的擴展點
按照分層的觀點,下層不允許依賴上層,然而業務對象卻是協作完成某個目的的。
而且只要業務對象需要維護,就需要相關的Act。
例如:銀行中的存錢業務,參考上面的分層,我們把它放入Operation層。
在存錢的業務中,我們需要檢查該客戶是否做了掛失。而掛失協議我們是放在Commitment層。
顯然,Operation層不能直接調用Commitment層的協議。
DIP模式發話了“用我”。
在Operation層中定義Commitment層接口,和一個工廠,使用反射實現這種調用。在Act中調用。
1
abstract public class ActImpl
2
extends abstractActImpl
3
implements Act
4

{
5
public virtual void run()
6

{
7
doPreprocess();
8
doRun();
9
doPostprocess();
10
}
11
abstract public doPreprocess();
12
abstract public doRun();
13
abstract public doPostprocess();
14
}
15
16
public interface CustomerCommitment
17

{
18
void affirmCanDo();
19
}
20
21
abstract public class CustomerActImpl
22
extends ActImpl
23
implements CustomerAct
24

{
25
26
public override void doPreprocess()
27

{
28
29
//擴展點
30
CustomerCommitment customerCommitment = CustomerCommitmentFactory.create(this);
31
customerCommitment.affirmCanDo();
32
33
}
34
35
}
36
37
public interface InnerCustomerCommitment
38

{
39
void affirmCanDo(CustomerAct customerAct);
40
}
41
42
public class CustomerCommitmentImpl implements CustomerCommitment
43

{
44
private CustomerAct customerAct;
45
46
public CustomerCommitmentImpl(CustomerAct customerAct)
47

{
48
this.customerAct = customerAct;
49
}
50
51
public void affirmCanDo()
52

{
53
54
//通過配置得到該customerAct對應需要檢查的客戶約束,包括協議,逐一檢查。
55
DomainObjectCollection commitmentTypes = CustomerCommimentRepository.findByBusinessType(customerAct.getBusinessType());
56
57
58
foreach( CommitmentType typeItem in commitmentTypes )
59

{
60
InnerCustomerCommitment commitment = getCommitment(typeItem);
61
commitmentItem.affirmCanDo(customerAct);
62
}
63
64
}
65
}
66
67
public class CustomerLostReportAgreementChecker implements InnerCustomerCommitment
68

{
69
public void affirmCanDo(CustomerAct customerAct)
70

{
71
Check.require(customerAct.getCustomer() != null,"客戶不存在");
72
73
CustomerLostReportAgreement customerLostReportAgreement =
74
CustomerLostReportAgreementRepository.find(customerAct.getCustomer());
75
76
if(customerLostReportAgreement != null)
77

{
78
agreement.affirmCanDo(customerAct);
79
}
80
81
}
82
}
83
84
public class CustomerLostReportAgreement
85

{
86
87
public void AffirmCanDo(CustomerAct customerAct)
88

{
89
if(customerAct.getOccurDate <= expiringDate)
90
throw new CustomerLossReportedException(customer);
91
}
92
93
}
94
同樣道理,可以對其他上層的對象使用DIP使依賴倒置。
比如:電信計算費用。就可以通過在CustomerAct的doRun中插入擴展點來實現。
這樣復雜的計費算法就被封裝在接口之后了。可以分配另外的人員來開發。
業務活動的流程仍然清晰可見。
是啊,這正是接口的威力,大多數的設計模式不也是基于這種原理嗎?
還有在Act上的擴展點可以分為兩類,顯式的和隱式的。
電信費用的計算就是顯式的,因為CustomerAct需要知道計算的結果,用來從帳戶中扣除金額。
而檢查掛失協議是隱式的,CustomerAct可以對此一無所知。
通過在Act上的擴展點,我們可以向上擴展。
這仿佛是在樹枝上種木耳,呵呵。
DIP VS Facade
對于上面的情況,另外一種方法是使用Facade。
讓我們比較一下兩者。
簡要說明一下Facade的做法:
1
abstract public class CustomerActImpl
2
extends ActImpl
3
implements CustomerAct
4

{
5
6
public override void doPreprocess()
7

{
8
9
//注意:這里傳遞的參數,會使得用Facade方式的人大傷腦筋。
10
//按照掛失的要求目前傳遞getBusinessType(),getCustomer(),getOccurDate()就夠了
11
//但是對于所有的CustomerCommitment這些參數就不一定夠了。
12
//比如:客戶可能簽訂指定員工協議。(指只允許協議中指明的員工能操作的業務)
13
//那么該接口需要添加getOperator()參數。
14
//接口變得不穩定。
15
CustomerCommitmentManager.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中通過配置達到該目的。
要做到業務層不依賴于數據訪問層,同樣借助接口來完成。
在業務層定義數據訪問的接口,為了方便,可以使用一個類來封裝這些操作。
1
public interface CustomerFinder
2

{
3
Customer findByID(ID id);
4
Customer findByCode(String code);
5
DomainObjectCollection findByName(String name);
6
7
}
8
9
public class CustomerRepository
10

{
11
private static CustomerFinder finder = null;
12
private static CustomerFinder getFinderInstance()
13

{
14
if (finder == null)
15

{
16
finder = (CustomerFinder)FinderRegistry.getFinder("CustomerFinder");
17
}
18
return finder;
19
}
20
21
public static Customer findByID(ID id)
22

{
23
Customer obj = getFinderInstance().findByID(id);
24
Check.require(obj != null,
25
"未找到ID為:" + id.toString() +
26
"對應的 Customer。");
27
return obj;
28
}
29
30
}
31
在數據訪問層實現這些接口。因為是數據訪問層依賴業務層,所以你可以采用多種技術來實現,
使用hibernate這樣的開源項目,或者手工編寫Mapper。
ID id
另外一個有爭議的問題是Domain層是否要引入與業務無關的ID來標識不同的對象呢?
我的經驗是在業務層引入ID的概念會使很多事情變得方便些。
如:Lazyload。
這是否不屬于業務的范疇?是在概念上不屬于業務。但在業務上
不是沒有對應的概念。
例如:保存客戶定購信息的訂單,作為標識的就是訂單號,這是給人使用的。
在使用電腦后,我們也給對象一個它能理解的統一標識,這就是ID。
另外不要使用業務上的概念作為主鍵和外鍵,因為它們本來就不是數據庫的概念。
否則,會使得業務概念同數據庫的概念混淆起來。
ID的使用通常會選擇效率較高的long類型。
不過我們的實現走得更遠,我們將其封裝為ID對象。
Service層
現在我們向上看看將業務層包裹的服務層。
服務層是架設在應用層和業務層的橋梁,用來封裝對業務層的訪問,因此
可以把服務層看作中介,充當兩個角色:
1.實現應用層接口要求的接口;
2.作為業務層的外觀。
服務層的典型調用如下:
1
public interface CustomerServices
2

{
3
void openCustomer(CustomerInfo cutomerInfo);
4
void customerLostReport(String customerCode,Date expiringDate,String remark);
5
CutomerBasicInfo getCutomerBasicInfo(String customerCode);
6
7
}
8
9
public class CustomerServicesImpl
10
extends ServiceFacade
11
implements CustomerServices
12

{
13
14
public void openCustomer(CustomerInfo cutomerInfo)
15

{
16
try
17

{
18
init();
19
20
OpenCustomerAct openCustomerAct =
21
new OpenCustomerAct(customerInfo.name,
22
customerInfo.code,
23
customerInfo.address,
24
customerInfo.plainpassword
25
26
);
27
openCustomerAct.run();
28
29
commit();
30
}
31
catch(Exception e)
32

{
33
throw ExceptionPostprocess(e);
34
}
35
}
36
37
public void customerLostReport(String customerCode,Date expiringDate,String remark)
38

{
39
try
40

{
41
Check.require(customerCode != null && customerCode != "",
42
"無效的客戶代碼:" + customerCode);
43
init();
44
45
CustomerLostReportAct customerLostReportAct =
46
new CustomerLostReportAct(customerCode,
47
expiringDate,
48
remark);
49
customerLostReportAct.run();
50
51
commit();
52
}
53
catch(Exception e)
54

{
55
throw ExceptionPostprocess(e);
56
}
57
}
58
59
public CutomerBasicInfo getCutomerBasicInfo(String customerCode)
60

{
61
try
62

{
63
Check.require(customerCode != null && customerCode != "",
64
"無效的客戶代碼:" + customerCode);
65
init();
66
Customer customer = CustomerRepository.findByCode(customerCode);
67
68
//這里選擇的是在CustomerRepository外拋出CustomerNotFoundException異常,
69
//另一種方法是在CustomerRepository中拋出CustomerNotFoundException異常。
70
//因為CustomerRepository在于通過客戶代碼查找對應的客戶。至于是否應該拋出
71
//異常則交給業務層或服務層來處理。
72
//這里有很微妙的區別,拋出CustomerNotFoundException應該是誰的職責呢?
73
//你的想法是什么?
74
if(customer == null)
75
throw new CustomerNotFoundException(customerCode);
76
77
CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer);
78
return cutomerBasicInfo;
79
}
80
catch(Exception e)
81

{
82
throw ExceptionPostprocess(e);
83
}
84
}
85
86
87
}
88
89
服務層的代碼很簡單,不是嗎?
90
91
上面的代碼可以通過AOP進一步的簡化。使用AOP實現我希望代碼象下面這樣簡單。
92
public class CustomerServicesImpl
93
implements CustomerServices
94

{
95
96
public void openCustomer(CustomerInfo cutomerInfo)
97

{
98
OpenCustomerAct openCustomerAct =
99
new OpenCustomerAct(customerInfo.name,
100
customerInfo.code,
101
customerInfo.address,
102
customerInfo.plainpassword
103
104
);
105
openCustomerAct.run();
106
}
107
108
public void customerLostReport(String customerCode,Date expiringDate,String remark)
109

{
110
Check.require(customerCode != null && customerCode != "",
111
"無效的客戶代碼:" + customerCode);
112
CustomerLostReportAct customerLostReportAct =
113
new CustomerLostReportAct(customerCode,
114
expiringDate,
115
remark);
116
customerLostReportAct.run();
117
}
118
119
public CutomerBasicInfo getCutomerBasicInfo(String customerCode)
120

{
121
Customer customer = CustomerRepository.findByCode(customerCode);
122
if(customer == null)
123
throw new CustomerNotFoundException(customerCode);
124
125
CutomerBasicInfo cutomerBasicInfo = CutomerBasicInfoAssembler.create(customer);
126
return 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
老妖 閱讀(407)
評論(0) 編輯 收藏