角色建模讀書筆記

作者:林星

                                         

這篇文章源自於 Martin Fowler 的角色建模的文章,在吸納大師觀點的同時,加入了自己的一些看法。故將之整理為一篇讀書筆記。

物件導向技術其實是一種以現實世界的自然觀點看待軟體代碼的方法。原先的軟體的編程方式是程序式的,但是這種程序式的編程方式沒有辦法處理過多的代碼行,因為人的思考能力是有限的,很少有人同一時刻看的代碼能夠超過 500 行以上的,當然,有特異功能者除外。人們發明了非常多的方法來使程序式編程方法更加的方便,其基本的思路都是通過一些技術,對大段的代碼行進行分割,例如代碼塊、函數、模組、工程、靜態聯編、動態聯編等等。但是直到物件導向技術出現之前,並沒有出項完整、徹底的解決方案。

我們剛開始學習物件導向的時候,總是要瞭解上一堆狗啊,哺乳動物啊之類的關係,其實這些都是為了向我們展示物件導向是符合人類對世界的認識的,也就是符合人類的世界觀的。但是糟糕的是,物件導向技術看起來似乎應該是符合人們的世界觀的,但是好像卻要比程序式編程技術要難的多。好吧,我們撰寫此文的目的就是為了解決這個似是而非的問題,揭開物件導向的面紗。你會發現,物件導向其實就是這麼自然。

我們所有討論的例子都集中在企業應用中,一方面的原因是企業應用其實是人類社會的一個縮影,能夠靈活的運用物件導向技術來分析企業應用,那應該說你已經到了老鳥的級別了。另一個方面則是因為個人的原因,本人並沒有接觸過其他性質的系統,所以這也是沒有辦法的事情。

首先我們要談的是物件所扮演的角色的問題。我們說狗是物件扮演的角色,文具也是物件扮演的角色。物件能夠類比出現實世界中各種各樣的概念、事物、事件。我們把扮演特定角色的物件稱為物件角色(object role)。現在我們先拋出第一個問題,一個公司中擁有各種各樣不同的雇員,包括工程師(engineers)、銷售員(salesmen)、主管(directors)、會計(accountants)。我們如何把這些現實世界中的概念(或者說是事物,這取決於你如何看待這些名詞,以及你的需求)對應到物件導向中?或者說,我們如何用物件來表示這些概念?類似的問題還有很多,但我們這奡N不多說了。值得注意的是,我們這奡ㄗ鴘漯咱韞i以理解為類別,其實還包括類型,不過在有些語言中類型也被當作類別來處理,而類別的具體實例,我們會稱之為物件實例。

第一種處理方法,也是最簡單的處理方法――使用單個物件來表示統一的概念。這是下文中所有方法的基礎,因此不要小看它。這是什麼意思呢?也就是說,不論你要處理的概念是工程師,還是主管,或是會計。我都用

public class Employee

來表示它們。那麼我應該如何分辨員工是工程師,還是會計呢。好,最簡單的方法是使用一個 string 來表示員工職業的分別:

public class Employee{
  String employeeType;
}

當然,其他的類型資訊也是允許的:

瀆ublic class Employee{
JobDescription employeeType;

這堛 JobDescription 是另一個用於描述職位資訊的類別。是不是非常的簡單?這只是一個開始。但是要注意,根據我的經驗,越是簡單的處理方式其實越是實用。對沒有太大差別的概念使用統一的物件來表示它們,並使用欄位來表示概念的區別,對於大部分的需求都已經足夠了。而困擾我們遲遲不敢做出簡單的決策的原因往往是對未來變化的恐懼,也許你認為在未來用戶可能會修改他們的需求,要求加入新的業務邏輯,處理不同的職位。例如,工資的演算法依賴於不同的職位。對未知事物的恐懼往往導致我們進行過分的設計。放輕鬆些,變化並沒有什麼了不起的,就像下文提到的,只要介面設計足夠的穩定,變化不足畏懼,適應變化正是物件導向所擅長的。因此,記住這一點,永遠只是處理當前的需求,不要進行過分設計。這也是敏捷方法所提倡的。

好吧,這時候我們需求開始發生變化了。工程師、主管、銷售員以及會計這些員工之間出現了非常大的差異。因此我們嘗試使用另一種方法:每個單獨的概念對應到一個獨立的物件上。這樣,我們就有了工程師物件、主管物件、銷售員物件、會計物件。為簡便起見,這堛漸N碼我就不寫了,和前一個方法的代碼非常的類似,只是代碼的量變大了而已。相信很多程式師對這種處理方法都不陌生。這種方法的好處是它能夠把不同類型員工給分開,並使得基於不同職業進行業務邏輯處理成為可能,而且不會導致對象間耦合度的上升。

有利就必有弊。這種方法有兩個致命的缺點:第一種是重複代碼的產生,我們注意到,不論是工程師,還是主管,他們都屬於員工,因此他們都有姓名、性別、年齡、住址等相同的個人資訊。由於使用的不同的物件來建模它們,這些屬性不得不重複好多遍。就算我們有這個耐心把這些屬性一一放到各個物件中,但噩夢仍然沒有結束,如果我們那需要增加一些屬性,而這些屬性恰巧是屬於各個物件所共有的。那麼我們不得不在各個物件中依次加入這些屬性。天哪,總有一天我會瘋掉的。什麼,繼承?你說對了,這就是我們要使用繼承機制的一大原因。關於這一點,我們在下文討論。

現在我們來看第二個缺點完整性不足。舉一個例子,如果有一位員工,他既是工程師,又是主管。如何處理這種情況?我們只好為這個人建立兩個物件,但是我們就沒有辦法分辨出它們其實表示的是同一個人。如果工程師和主管之間存在依賴關係的話,那可就糟糕了。這種問題在企業應用中時有發生,最經典的例子是供應商和客戶同屬於一個客戶的情況。在 http://www.erptao.org 上有一篇文章就是專門討論對這種情況的解決方法的。大家可以參考。

總的來說,這種方法不算是一種好的處理思路,能不用就不用吧。不過仍然是要具體情況具體分析。

鐺鐺鐺鐺!繼承機制進場!我們回到第一個問題結束時留下的疑問,現在我們希望充分利用物件導向的三大特性之一的繼承來解決我們遇到的問題。好,我們首先定義一個員工物件(或是其他什麼名字),這個物件定義了員工的公用屬性以及公用邏輯。然後我們再讓工程師物件繼承自員工物件,在工程師物件中實現工程師特有的屬性和邏輯。好,依法炮製,我們得到了一個父物件-員工,以及三個子物件。如果我們需要處理的層次仍然停留在員工層次上,我們只需要處理員工物件,如果我們需要處理特定的員工,那我們就單獨處理員工物件的子物件。如果我們有一位員工既是工程師,有時主管,那我們就也可以創建相應的子物件,如果我們希望增加退休員工的概念,我們也可以再加一個子物件。如果 ... 等等,還如果呢,出問題了。只要是有物件導向編程經驗的程式師都知道單根繼承和多重繼承的區別,而現在的很多語言都是不支援多重繼承的。那麼,我如果要在不支援多重繼承的語言中處理員工既是工程師又是主管的例子,我就需要創建一個工程師/主管的子物件,那麼不用多少時間,我們手上的子物件的數量就會急劇上升。這就是典型的子類別爆炸的情形。此外,繼承的機制一般都是靜態的,而不是動態的,也就是說,我不能夠在運行時隨便改變物件的類型(注意,和多態的概念不同)。

好吧,是時候談點抽象的概念了。我記得 Martin Fowler 在他的分析模式一書中指出,分析問題應該站在概念的層次上,而不是站在實現的層次上什麼叫做概念的層次呢?簡單的說就是分析物件該做什麼,而不是分析物件怎麼做。前者屬於分析的階段,後者屬於設計甚至是實現的階段。在需求工程中有一種稱為 CRC 卡片的玩藝兒,是用來分析類別的職責和關係的,其實那種方法就是從概念層次上進行物件導向設計。因此,如果要從概念層次上進行分析,這就要求你從領域專家的角度來看待程式是如何表示現實世界中的概念的。下面的這句話有些拗口,從實現的角度上來說,概念層次對應於合同,合同的實現形式包括介面和基礎類別。簡單的說吧,在概念層次上進行分析就是設計出介面(或是基礎類別),而不用關心具體的介面實現(實現推遲到子類別再實現)。結合上面的論述,我們也可以這樣推斷,介面應該是要符合現實世界的觀念的

好,現在我們設計出的介面看起來像是這樣的:

檯nterface Person {
瀆ublic String name();
瀆ublic void name(String newName);
瀆ublic Money salary ();
瀆ublic void salary (Money newSalary);
瀆ublic Money payAmount ();
瀆ublic void makeManager ();
 
檯nterface Engineer extends Person{
瀆ublic void numberOfPatents (int value);
瀆ublic int numberOfPatents ();
 
檯nterface Salesman extends Person{
瀆ublic void numberOfSales (int numberOfSales);
瀆ublic int numberOfSales ();
 
檯nterface Manager extends Person{
瀆ublic void budget (Money value);
瀆ublic Money budget ();

介面看起來不錯,但是如何實現它呢,大致的做法有三種:內部標示(Internal Flag)、隱藏委託(Hidden Delegate)、 以及狀態物件(State Object

下面的例子只實現了銷售員部分的代碼,沒有使用到多重繼承或是動態繼承,但卻較好的解決了前述的問題:

public class PersonImpFlag implements Person, Salesman, Engineer,Manager{
// Implementing Salesman
public static Salesman newSalesman (String name){
PersonImpFlag result;
result = new PersonImpFlag (name);
result.makeSalesman();
return result;
};
public void makeSalesman () {
_jobTitle = 1;
};
public boolean isSalesman () {
return _jobTitle == 1;
};
public void numberOfSales (int value){
requireIsSalesman () ;
_numberOfSales = value;
};
public int numberOfSales () {
requireIsSalesman ();
return _numberOfSales;
};
private void requireIsSalesman () {
if (! isSalesman()) throw new PreconditionViolation ("Not a Salesman") ;
};
private int _numberOfSales;
private int _jobTitle;
}

這部分的代碼使用了內部標示。在這個例子中我們用了一個單獨的員工類別來實現四個介面。因此我們需要對不同的子物件進行區分。newSalesman makeSalesman 以及 isSalesman 方法就是這種區分子物件的方法的具體實現。注意,其中 newSalesman 是一個靜態方法,它起到了一個構造器的作用。此外,這堥洏峈漱隤k屬於顯式類型方法(Explicit Type Method)。為了保證銷售員的實現代碼僅為銷售員物件所呼叫,上面的代碼中使用一個私有方法 requireIsSalesman 來作為守衛,如果有非銷售員物件呼叫了銷售員介面的方法,就會引發一個運行時錯誤。對於顯式類型方法來說,一般的名稱可以使用 isTypename 或是 beTypename,這種處理方法的好處是介面比較簡單,但是如果加入新的類型的時候,基礎類別的介面必須跟著變化。

從需求上來看,payAmount 操作應該是一個多型性操作,由員工基礎類別定義,但由不同的子物件來實現。我們的實現方法是使用了一個 case 語句。一般來說,物件導向技術是不提倡使用 case 語句的,但這堥洏峊托o無傷大雅,因為 case 語句是隱藏在員工類中的。因為我們的處理方式不能夠使用到物件導向的多態機制,因此我們只好採用內部 case 語句的方式來實現這種多型性。

public Money payAmount (){
if (isSalesman()) return payAmountSalesman();
if (isEngineer()) return payAmountEngineer();
throw new PreconditionViolation ("Invalid Person");
};
private Money payAmountSalesman () {
return _salary.add (Money.dollars(5).multiply(_numberOfSales));
};
private Money payAmountEngineer () {
return _salary.add (Money.dollars(2).multiply(_numberOfPatents));
};

可以看到,內部標示的做法為複雜的分類提供了一種可能的實現,但是它卻孕育了一個複雜的員工類別,它囊括了所有子物件的資料和行為,並提供了選擇機制如果不對它加以控制,那麼它將會變成一個龐然大物。可以看出,這種做法雖然利用到了物件導向的思路,但是實現的方法還是程序式的,但是它有自己的很多好處。所以,只要員工類別不至於失控,這種設計思路還是非常有價值的。在應用系統的很多地方,都可以採用該方法或是該方法的變種。應該認識到,介面導向編程和物件導向編程還是有差別的。

結束了內部標示的示例後,我們開始討論另一種做法:隱藏委託(Hidden Delegate

public class PersonImpHD implements Person, Salesman, Engineer,Manager{
// implement manager
public void makeManager () {
 _manager = new ManagerImpHD();
};
public boolean isManager (){
 return (_manager != null);
};
private void requireIsManager () {
 if (! isManager()) throw new PreconditionViolation ("Not a Manager") ;
};
public void budget (Money value) {
 requireIsManager();
 _manager.budget(value);
};
public Money budget () {
 requireIsManager ();
 return _manager.budget();
};
private ManagerImpHD _manager;
}
 
class ManagerImpHD {
public ManagerImpHD () {
};
public void budget (Money value){
 _budget = value;
 };
public Money budget (){
 return _budget;
};
private Money _budget;
}

使用這種隱藏委託的方法,我們把和經理相關的行為和資料移到了經理物件中,從而大大簡化了員工類別。這種方法非常適用於角色的子類別包含了很多的額外行為和特性的情況。但是這種方法還是沒能夠克服原來問題的一個缺陷-員工類別仍然持有角色子物件的所有介面,而且員工類別必須處理對不同方法的選擇,即決定呼叫何種方法,傳遞何種參數。這樣我們又理所當然的想到了繼承機制,它能夠很好的處理多態,為什麼我們不能夠利用物件導向語言提供給我們的便利之處呢?

於是乎,我們想到了利用四人幫(GOF)在設計模式一書中談到的狀態物件模式的設計思路。如果我們的子物件比較穩定,相互之間也不存在聯集,那麼採用這種模式預先劃分好子物件是非常好的一種思路。當然,我們這堣w經吸髓知味,知道說隱藏物件的技巧有助於向用戶端隱藏具體的實現。因此,我們繼續沿用這種思路,這樣,我們的思路就基本上可以這樣描述:

為每一個角色定義一個物件,這些物件對用戶端不可見,因此稱為隱藏物件。為這些隱藏物件定義一個父類別,一方面可以利用繼承的多態機制實現方法的選擇和類型安全,另一方面可以把角色的通用行為移至父類中,以實現代碼的重用。

設計一個會話類別,實現所有的角色介面,但其自身不需要處理實現,而是把具體的實現委託給前面設計好的繼承樹。

從下面的代碼中,我們可以清晰的看到我們提出的這幾點步驟是如何貫徹到代碼中的:

public class PersonImpHD implements Person, Salesman, Engineer,Manager{
public static Salesman newSalesman (String name){
PersonImpHD result;
result = new PersonImpHD (name);
result.makeSalesman();
return result;
};
public void makeSalesman () {
_job = new SalesmanJobHD();
};
public boolean isSalesman () {
return _job.isSalesman();
};
public void numberOfSales (int value){
_job.numberOfSales(value);
};
public int numberOfSales () {
return _job.numberOfSales();
};
private JobHD _job;
};
 
abstract public class JobHD {
private void incorrectTypeError() {
throw new PreconditionViolation("Incorrect Job Type");
};
public boolean isSalesman () {
return false;
};
public void numberOfSales (int value) {
incorrectTypeError();
};
public int numberOfSales (){
incorrectTypeError();
return 0;//value not returned since exception is thrown,
compiler needs return
};
}
public class SalesmanJobHD extends JobHD{
public boolean isSalesman () {
return true;
};
public void numberOfSales (int value){
_numberOfSales = value;
};
public int numberOfSales () {
return _numberOfSales;
};
private int _numberOfSales = 0;
}

在員工類別中幾乎不存在任何的實現代碼,所有的請求都委託給了隱藏物件們。從這堛漕狺l還不能夠明顯的看出多態的優勢,我們再看一個例子:

public class PersonImpHD implements Person, Salesman, Engineer,Manager{
public Money payAmount (){
return _job.payAmount(this).add(managerBonus());
};
abstract public class JobHD {
abstract public Money payAmount (Person thePerson);
public class SalesmanJobHD extends JobHD{
public Money payAmount (Person thePerson) {
  return thePerson.salary().add (Money.dollars(5).multiply(_numberOfSales));
};

這一段計算薪水的代碼就完全體現了多態的優勢了。這媮晹酗@些小技巧,可以看到,payAmount 方法需要用到 person 類別的資料,因此,person 類別在呼叫 payAmount 方法時將自己作為參數傳遞給了 JobHD 類別。這種方式稱為自委託(Self Delegation[Beck]

可以看到,對新增加角色的支援也同樣簡單,只需要從 JobHD 再繼承一個子類別下來就行了。由於介面是由 person 類別來實現的,因此所有的改動都是在背後完成的,這可能就是隱藏物件的優勢所在了。對於用戶端來說,對於任何一個介面,都只需要對 person 進行實例化,然後根據不同的介面定義的不同的標準來呼叫就可以了,至於所有的實現細節,用戶端根本就不知道。當然必須增加介面,但現在無論是哪一種平臺都支持這種機制。因此,我們可以對從內部標誌模式到隱藏委託模式,再到狀態物件模式做一個簡單的總結:

1.           介面和實現是分離的。

2.         介面符合現實世界的世界觀,因此介面應該相對穩定。

3.         實現需要根據需求的複雜度,可能的變化、工作量的大小等因素來考慮使用何種方式。

4.         用戶不需要知道具體的實現,所有方法選擇、類型判定,演算法實現等操作都是在用戶一無所知的情況下進行的。

5.         使用一個外觀類別(person類別)來達到上面一點的效果。可以根據自己的因素,來考慮在外觀類別中實現多少細節。

6.         外觀類別中的實現細節可以完全移到新的類別中,這取決於我們那的實際情況。

7.         三種不同的處理方法是可以根據需求的複雜程度來選擇的,當然,相應的投入也會增大。

應該說,這種處理思路是非常自然的介面導向的設計思路,雖然不同的方法有著不同的實現技巧,但是主要的思路相似的,先從簡單的處理出發,然後慢慢的對結構進行優化。這種思路使用於所有的軟體設計。在這個例子中,實際上,在現實的軟體發展中,一般不會定義三種的處理方式,而只會選取最優的那一種-即物件狀態模式,因為模式一旦優化整理完畢,再次實現所花費的工作量其實是不大的,當然前提是你的模式整理工作要做的好。而且這種做法有助於保持軟體、軟體團隊的一致性。因此,我們在處理一個問題時,可以先把所有的實現放在一個單獨的類別中(該例中為 person 類別),然後再試著把屬性和方法從這個類別中剝離出來,讓不同的類別來負責不同的職責,最後將設計完畢的方法整理為模式,在軟體團隊內部推廣,並在實踐中維護、改進該模式,以使其適合更複雜多變的需求。就像是隱藏物件一樣,這種思路也是隱藏在物件導向設計背後的設計思路,值得我們花時間來掌握它。

需求說變就變,我們原先對單個人承擔多個角色的擔心還是發生了。按照上面的設計,要處理這種實現是很麻煩,很不自然的。因為你必須為一個現實中的人創建兩個 person 物件。這是非常要命的,尤其是角色之間有相互衝突、相互呼叫的業務邏輯的時候。它可能會對整個設計產生致命的影響。看來,我們又有必要揮舉無情重構的大刀來向我們的代碼砍去了。

無情的重構是 XP 非常提倡的最優實踐之一。應該承認,重構的思路是非常正確的。但是重構是不是一定需要做到如此徹底的程度呢,這方面我有著不同的看法。對代碼的每一次修改都可能造成成本的上升,當一個團隊,而不是一個個人在開發軟體的時候,你更多需要考慮的是成本、質量、實現的辨正的統一。尤其是你需要對團隊中的每一位程式設計師非常的瞭解,瞭解他們的能力,瞭解他們的性格。因此不顧一切的重構在現實中往往會遇到各種各樣的阻力,但是如果重構最終達到的效果並不十分明顯,或是遠低於貫徹它所付出的代價的時候,重構就是不合適的。但是應該說,在適當的情況下的這種實踐是非常有必要的,特別是需要研究代碼重用的時候,比如我們現在討論的對模式的改進的問題。這種情況下代碼的每一步優化都將極大的提高生產力。

在稍微離題之後,重新回到我們的討論之中。我們已經知道需求要求我們為一個物件表示多種角色。所以, person personRole 之間的關係演變為一對多的關係,這樣就能夠解決一個員工可以擁有多種職位的需求。可以想像到,這種做法的一個不好的地方是,用戶需要開始瞭解部分的實現了,因為原先定義的介面已經不合用了。在這個例子中,用戶除了需要知道員工自身(person 類別),還需要知道職位的資訊(personrole),這樣才能夠處理員工和職位之間一對多的關係。在這種情況下,我們對職位資訊的處理,還是可以採用上文所討論的顯示的方式,例如 isSalesman() 方法。但是由於部分實現已經暴露給用戶端了,這種方式已經失去了它的存在意義。更好的方法是採用參數化的 hasType(String) 方法。這種方法的優點是為各職位物件定義了統一的邏輯介面,同時可以動態的增加職位物件,而對介面不會有影響。和上面的所有討論的方法類似的,有一利就有一弊。這種方法定義的介面不夠清晰,至少我們需要瞭解 string 參數的定義。此外,編譯器不再為我們執行類型檢查的工作,這部分的壓力移到了程式師的身上。因此,這種動態的參數化技巧的使用是需要非常小心的,只有確保它為你帶來的利益能夠超出它的成本的時候才能夠使用它。這種參數化的方法的命名一般可以採用 hasType( typename) 或是 beType( typenam ) 的方法。

class Person {
public void addRole(PersonRole value) {
  _roles.addElement(value);
};
public PersonRole roleOf(String roleName) {
  Enumeration e = _roles.elements();
  while (e.hasMoreElements()) {
    PersonRole each = (PersonRole) e.nextElement();
    if (each.hasType(roleName)) return each;
  };
  return null;
};
private Vector _roles = new Vector();
  
}
 
public class PersonRole{
public boolean hasType (String value) {
  return false;
};
public class Salesman extends PersonRole{
public boolean hasType (String value) {
  if (value.equalsIgnoreCase("Salesman")) return true;
  if (value.equalsIgnoreCase("JobRole")) return true;
  else return super.hasType(value);
};
public void numberOfSales (int value){
  _numberOfSales = value;
};
public int numberOfSales () {
  return _numberOfSales;
};
private int _numberOfSales = 0;
}
// To set a salesman’s sales we do the following
Person subject;
Salesman subjectSalesman = (Salesman) subject.roleOf("Salesman");
subjectSalesman.numberOfSales(50);

我們來分析這段代碼。和以往的代碼不同的是,介面已經消失了。因為在原先的需求中,我們是不關心員工和職位之間的關係的,換言之,我們認為員工和職位是一體的,只是不同的職位對於我們來說有著不同的處理方法。但是根據現在的需求,我們需要處理員工和職位兩者的關係,對於我們來說,員工和職位已經是兩個並行的類別樹了。所以,我們看到用戶端的呼叫代碼中是先聲明員工,再呼叫員工的 roleOf 方法來呼叫員工所從事的某種職位。因此,用戶端需要知道職位的資訊,才能夠正確的獲得職位物件。注意 hasType 方法中的方法檢查,它是根據多態的基本原則來實現的,對於 Salesman 類型和 Salesman 的父類別都返回真,其實,對父類別類型的判斷也可以放到父類別的方法中。

上面的例子看不出具體的應用,我們可以考慮一種情況,我們需要對公司中的所有銷售人員(可能有人身兼多職)進行一項業務邏輯處理。我們的代碼可以寫成這樣:

Enumeration e = persons.elements();
while (e.hasMoreElements()) {
   Person each = (Person) e.nextElement();
   if (each.hasRole("Salesman")) {
     Salesman sm = (Salesman) each.roleOf("Salesman");
 
       //其他的處理

hasRole 方法是一個新增加的函數,其實現的機制類似於 roleOf 方法。

好吧,使用現在的代碼,我們已經能夠處理員工和職位之間一對多的情況了。那麼,我們有沒有想過另一種的情況呢,員工和職位之間是多對一的情況。比如最簡單的例子是 114 1000 的人工台。對於用戶來說,只存在接線員這個角色,而每次接線員這個角色對應的人是不同的。因此,我們的視角就轉移了,轉移到了角色上,類似的,我們同樣可以根據需求來改進我們的設計,但是介面一定已經不同了,因為需求的變化非常之大。因此,我們再一次深入到設計的背後,思考為什麼我們採用 A 設計,而不是 B 設計。這些都是有根據的,決不是因為A 設計比較 Cool 之類的原因。最大的設計選擇的影響因素就是需求,包括功能性需求和非功能性需求(約束條件)Martin Fowler 感歎到,設計其實是一種權衡策略,一針見血的指出了設計的本質。結合到我們的例子中,為什麼我們採用的設計是在員工類別中增加一個角色列表,而不是相反的情況呢。這是因為需求要求我們這樣做,職位是員工的某一類別屬性,雖然這類的屬性比較特殊,但員工物件仍然處於核心的地位。假設我們的需求發生了變化,對員工的資訊不再關心,而轉而關注職位資訊,那樣我們的設計就會截然不同。如果在處理資訊系統中,往往職位和員工都佔有很重要的位置,大量的資訊和邏輯都是基於這組概念的。這樣我們可以就需要在員工和職位之間設計雙向關聯,來表現他們之間的多對多的關係。最簡單的例子是,一般的資訊處理是從員工角度出發的,但是人力資源系統的職位處理就需要從職位角度出發。還是那句話,關鍵還是取決於我們的視角,我們的需求

好吧,我們再次回到設計中,和你想像的一樣,我們需求又發生了變化。現在我們需要加入限制邏輯了。我們規定,一位員工不能夠從事兩份的工作性職位,但可以在一份工作性職位外再兼任一份管理性職位。

廢話少說,我們立刻開始修改我們的代碼:

class Person {
public void addRole(PersonRole value) throws CannotAddRole {
if (! canAddRole(value)) throw new CannotAddRole();
  _roles.addElement(value);
};
private boolean canAddRole(PersonRole value){
  if (value.hasType("JobRole")){
    Enumeration e = _roles.elements();
    while (e.hasMoreElements()) {
      PersonRole each = (PersonRole) e.nextElement();
      if (each.hasType("JobRole")) return false;
    };
  };
  return true;
};

非常精彩的代碼。我們來看一看這段代碼。我們要處理限制邏輯(約束條件),首先要解決的問題是瞭解清楚它的來源。因為這一類的設計往往源自於非功能性需求。限制邏輯的實現策略有非常多種。例如,把限制邏輯放在介面層,或是把限制邏輯用資料庫的 constraint 來實現。我個人的意見比較偏向於把限制邏輯放在業務層,就像我們的代碼所處理的那樣。原因有三:首先,介面層往往部署在大量的客戶機器上,邏輯的修改非常不方便;其次,不同資料庫的 constraint 有所差別,移植不便;最後,限制邏輯過於分散的話,會造成修改維護成本的上升。基於這幾點原因的考慮,我比較傾向於集中在業務層實現限制邏輯。當然,和以上的設計思路一樣,這還是要根據實際情況進行調整的。

下面一個考慮的因素是,限制邏輯放在哪些類別中,又該放在類別的哪個位置上。這是一直以來困擾我的一大問題,因為最經常遇到的一種情況是,限制條件往往是跨類別的-即它和多個類別相關,因此很難能夠決定它的適合位置。這塈痤馴X兩種解決建議。之所以說是解決建議,是因為這個問題是無解的,只能夠憑藉各位的經驗來做具體的判斷,但是有些建議能夠幫助大家做決定。

第一種建議是,和你的領域專家討論限制條件的位置。因為只有領域專家才有足夠的經驗和權威做出需求的決策。中間可以使用到一些技巧,例如 CRC 卡片,來幫助討論。一般在這種討論會上主要是把限制條件分配給某個或某幾個業務實體類別,要想真正確定其位置,還要結合建議二。

第二種建議是,如果遇到涉及到多個類別的限制條件,那麼也許意味著你需要設計某種體系結構來處理把這幾個類別整合起來。這種體系結構可能是繼承樹、管理類別、外觀類別的組合。例如我們的代碼就是這樣。如果沒有 person 類別,我們可以不知道把這個限制邏輯放於何處。

好,在討論了限制邏輯之後,我們又提出一個問題,這堛漸N碼中,限制邏輯只有一個,如果限制邏輯很多怎麼辦呢?這時候你可以考慮專門為限制邏輯設計一種方法(類似於 canAddRole),甚至是一個類別來集中基礎限制邏輯。但是如果你的限制邏輯太多,到了一種有礙觀瞻的地步的時候,也許正說明你的設計結構存在某種問題。比如,我們完全可以不使用限制邏輯方法就實現以上的需求:

class Person {
public void jobRole(JobRole value){
  _jobRole = value;
};
public PersonRole jobRole() {
  return _jobRole;
};
private JobRole _jobRole;
}

代碼是不是簡單了很多?因此某些時候,無情的重構還是有必要的,它能夠大大的簡化代碼。但是應該認識到,凡事都不可能一開始就完美無缺,一定存在一個進化的過程。對於代碼的來說,結構的優化始終都是最重要的。因此,設計程式如果能夠著眼於設計,著眼於代碼結構,那出產的代碼一定是高質量的

噩夢終於要到頭了,需求的最後一次變化是增加了職業分組的功能。這種情況是非常普遍的,比如每種職位需要屬於某個部門,這就是對職業進行某一種分組。從另外一個層面上來說,這其實是連接其他設計模組的橋樑。因為我們討論的模式雖然獨立,但是對於軟體來說,將各個獨立的模式有機的連接起來也是非常重要的。因此,在下面的代碼中,我們可以看看模式是如何擴展的:

class Person {
public void addRole(PersonRole value)throws CannotAddRole {
  _roles.addElement(value);
};
public PersonRole roleOf(String roleName, Group group) {
  Enumeration e = _roles.elements();
  while (e.hasMoreElements()) {
    PersonRole each = (PersonRole) e.nextElement();
    if ((each.hasType(roleName)) && (each.group() == group)) return each;
  };
  return null;
};
private Vector _roles = new Vector();
public class PersonRole{
protected PersonRole (Group group){
  _group = group;
};
public Group group(){
  return _group;
};
protected Group _group;
public class Salesman extends PersonRole{
public Salesman (Group group){
  super (group);
};

注意到,這堛熙s接設計是採用參數的方式進行的。分組作為一個參數傳遞給 person personrole。由於我們建立了 personrole group 之間多對一的關係,因此在 personrole 中設置了類型為 group 的私有變數來保存這種關係。這堙A我們認為職位和職位分組是一種單向的關係,因此不需要在group類別中同步的處理。同樣的,如果要加入限制條件的話,仍然需要考慮其實現位置。

我們想像一下,在 group 類別的背後,可能也隱藏著大量的內部實現類別或是協作類別。這些類別通過 group personrole 之間的關係連接在了一起。

小結:

簡單設計,不斷改進

美好的設計不可能一開始就完成,做事情的正確方式應當是循序漸進。記住簡單的原則,儘量不要為未來的需求而編程。與此同時,我們還需要處理變與不變之間的辨正關係。從需求中識別出變化的部分,以及不變的部分,對變化的部分進行抽象,識別出變化的表面之下的不變的內涵。這種思考方式就是抽象的思想。

改進包括兩種概念,一種是對功能的改進,一種是對代碼的改進。前者我們在下面的討論中會提到,XP 中的重構的思想有助於我們處理後者。重構要用的好,我的經驗是配合代碼復審會議來使用。從這樣的活動中總結出適合你自己的重構標準。我提倡小規模的、不斷進行的重構,反對大規模的重構。因為後者的風險過高。要形成團隊不斷重構的習慣並不是一件容易的事情,需要額外的動力才能夠實行。

設計來自於需求,設計的改進源自於需求的演變

所有的設計都不是孤立存在的,強制在自己的專案中應用設計模式,應用剛學到的物件導向技巧,都只是削足適履。設計應該從需求出發,並滿足需求。脫離需求的設計並不是一個好的設計。注意到,文章的前半部分討論的設計和後半部分討論的設計截然不同,因為需求發生了變化,但兩種設計都屬於優秀的設計。記住 Martin Fowler 的一句話:設計在於權衡。

需求在演進,相應的設計也需要演進。設計的演進往往被認為是一個痛苦的過程,不錯。但是還是有很多的辦法能夠解決這個問題的。下面一點的討論就是一種方法。

針對介面設計,介面應該能夠滿足現實世界觀

針對介面設計是一種新思想,它能夠幫助我們分離介面和實現部分,實際上,它們分離的是上文中提到的變化和不變的部分。介面是穩定的,因此它屬於不變的部分,實現是不穩定的,屬於變化的部分,隨著需求的演進而演進。設計一個穩定的介面是軟體設計中非常重要的部分,它能夠保證在用戶不察覺的情況下對需求進行擴展,對代碼進行升級。但是應該認識到,設計一個穩定的介面並不是一件容易的事情,而其中起到決定性因素的,不是物件導向的設計能力,而是領域經驗。換句話說,如果你對某個領域有深入的瞭解,能夠體會到領域中的抽象本質,那設計出的代碼自然會非常的優秀。因此,我們強調,介面要能夠符合現實世界觀。做到這一步需要時間和經驗