1. 程式人生 > >狀態模式在領域驅動設計中的使用

狀態模式在領域驅動設計中的使用

領域驅動設計是軟體開發的一種方式,問題複雜的地方通過將具體實現和一個不斷改進的核心業務概念的模型連線解決。這個概念是Eric Evans提出的,http://www.domaindrivendesign.org/這個網站來促進領域驅動設計的使用。關於領域驅動設計的定義,http://dddcommunity.org/resources/ddd_terms/,這個網站有很多的描述,DDD是一種軟體開發的方式:

  1. 對於大多數的軟體專案,主要的精力應該在領域和領域的邏輯。
  2. 複雜的領域設計應該基於一個模型。

DDD促進了技術和領域專家之前的創造性的合作,迭代地接近問題的概念核心。注意,在沒有領域專家的幫助時,一個技術專家可能不會完全理解一個領域的錯綜複雜,當然,沒有技術專家的幫助,一個領域專家實際上也不能應用它的知識到專案中。

大多數情況下,一個領域模型物件封裝一個內部的狀態,本質上是一個系統中某個元素的歷史,也就是,物件的操作是有狀態的。在那種情況下,物件保持它的私有狀態,這個狀態最終將影響他的行為。狀態設計模式可以乾淨地代表一個物件的狀態,處理它的狀態轉換。簡而言之,狀態模式是針對依賴於狀態做出行為的問題的解決方案。

很明顯,DDD和狀態設計模式息息相關。我對DDD是個新手,所以我將讓我們最出色的JCG夥伴 Tomasz Nurkiewicz,用一個例子來介紹使用狀態設計模式的DDD。

注意:為了提高可讀性,原始郵件被稍微重新編輯了下。

一些企業應用中的領域物件包含狀態的概念。狀態有兩個主要的特性:

  1. 領域物件的表現(如何響應業務方法)依賴於它的狀態。
  2. 業務方法可能改變物件的狀態,在一個特定的呼叫之後,物件可能表現出不同的行為。

如果你不能想象任何領域物件的例子,想象在租賃公司的一個Car實體。這個Car,儘管是同一個物件,但是它有一個附加的狀態標識,這對公司來說至關重要。這個狀態標識可能有三個值:

  1. AVAILABLE
  2. RENTED
  3. MISSING

很明顯,當一個Car處於RENTED或者MISSING的時候,是不能被租出去的,rent()方法應該失效。但是當Car被還回來的時候,它的狀態時AVALIABLE,對這個Car實體呼叫rent()方法應該與之前租借這個Car的使用者無關,將車的狀態改為RENTED。狀態標識(可能是一個字元或者是資料庫中的int型別)是物件狀態的一個例子,因為它影響著業務方法,反之亦然,業務方法也可以改變狀態。

現在,思考一個問題,你將怎麼實現這個場景,我相信,這個場景你在工作中遇到過很多次了。你有很多依賴於當前的狀態的業務方法和很多種的狀態。如果你喜歡面向物件程式設計,你可能立即想到繼承然後建立一個繼承自Car的AvailableCar,RentedCar和MissingCar。這看上去很好,但是不切實際地,特別是當Car是一個持久化物件的時候。實際上,這種繼承的方式不是一種好的設計:我們想要的不是改變整個物件,僅僅是物件的一條內部狀態,也就是說,我們不會替換一個物件,僅僅是改變它。也許你想在每一個依賴於狀態執行不同的任務的方法中用if-else-if-else這種層疊的方式。。。不要這麼做,相信我,那是程式碼維護的地獄。

相反,我們將不使用繼承和多型,但這是一個更聰明的方式:使用狀態模式。舉個例子,我選擇了一個叫做Reservation的實體,這個實體有下面這些狀態。

這個生命週期是非常簡單的“當Reservation被建立,它是NEW狀態。然後一些被授權的人可以接受這個Reservation,導致像座位被短暫地保留的事件發生,然後給人傳送一個e-mail,讓其為Reservation付費。再然後,使用者執行轉賬,錢到賬,列印票據,然後傳送第二封郵件給客戶。

你肯定已經意識到一些動作依據Reservation 當前的狀態有不同的效果。例如,你可以在任意時間取消reservation,但是依賴於Reservation當前的狀態,取消動作可能導致退款然後取消reservation,或者僅僅是傳送給使用者一個e-mail。一些動作不依賴於特定的狀態(如果使用者為一個已經取消的reservation付賬會怎麼樣)或者應該被忽略。如果你在每個狀態和每個業務方法中用了if-else結構,現在想象一下依據上邊的狀態機寫出業務方法將有多難。

為了解決這個問題,我將不解釋原始的GoF狀態設計模式。而是介紹一些在使用了java ENUM的功能之後,我對這個設計模式的一些改變的地方。代替為狀態抽象建立一個抽象的類或介面,然後為每一個狀態寫實現這種方式,我簡單的建立一個包含了所有可用的狀態的enum。

public enum ReservationStatus {
NEW,
ACCEPTED,
PAID,
CANCELLED;
}

然後我為所有依賴於狀態的業務方法建立了一個介面。把這個介面當做所有狀態的抽象基類,但是我們將以稍微不同的方式使用它。

public interface ReservationStatusOperations {
ReservationStatus accept(Reservation reservation);
ReservationStatus charge(Reservation reservation);
ReservationStatus cancel(Reservation reservation);
}

最後,Reservation領域物件,恰巧同時也是一個JPA實體(省略getters/setters)。

public class Reservation {
private int id;
private String name;
private Calendar date;
private BigDecimal price;
private ReservationStatus status = ReservationStatus.NEW;
//getters/setters
}

如果Reservation是一個持久的領域物件,他的狀態(ReservationStatus)很明顯也應該被持久化。這個觀察結果將使我們第一次體會到使用enum代替抽象類的巨大好處:JPA/Hibernate可以很容易地使用enum的名字或者順序的值(預設)序列化和持久化java enum到資料庫中。在原始的GoF模式中,我們將直接把ReservationStatusOperations 物件放到領域物件中,然後狀態改變時切換不同的實現。我建議使用enum然後僅改變enum的值。使用enum的另一個優勢(不是以框架為中心的但是更重要的)是所有可能的狀態在一個地方列出。你不必在你的原始碼中爬行來尋找所有的狀態基類的實現。所有的東西都能在一個地方被看到,一個逗號分隔的列表。

OK,深呼吸。現在我解釋一下所有的部分如何在一起工作,ReservationStatusOperations 中的業務操作為什麼返回ReservationStatus。首先,你必須回憶一下, enum究竟是什麼。他們不僅僅是像C/C++那樣的多個常量在一個名稱空間下的集合。在JAVA中,enum是多個類的閉集,繼承自一個公共的基類(例如ReservationStatus),最後繼承自enum類。所以當使用enum的時候,我們可能就使用了多型和繼承。

public enum ReservationStatus implements ReservationStatusOperations {
NEW {
public ReservationStatus accept(Reservation reservation) {
//..
}
public ReservationStatus charge(Reservation reservation) {
//..
}
public ReservationStatus cancel(Reservation reservation) {
//..
}
},
ACCEPTED {
public ReservationStatus accept(Reservation reservation) {
//..
}
public ReservationStatus charge(Reservation reservation) {
//..
}
public ReservationStatus cancel(Reservation reservation) {
//..
}
},
PAID {/*...*/},
CANCELLED {/*...*/};
}

雖然以上邊的方式寫一個ReservationStatusOperations 類很容易,但是從長遠來看,這是一個壞主意。不僅enum原始碼會極其的長(所有要實現的方法的數量等於狀態的數量乘以業務方法的數量),而且是一個壞的設計(所有狀態的業務邏輯在一個類中)。一個enum也可以實現一個介面,這個奇特的語法可能與沒有參加過SCJP exam 考試的人的直覺相反。我們將提供一個簡單的中間層,因為電腦科學中的任何問題都可以被另一箇中間層解決。

public enum ReservationStatus implements ReservationStatusOperations {
NEW(new NewRso()),
ACCEPTED(new AcceptedRso()),
PAID(new PaidRso()),
CANCELLED(new CancelledRso());
private final ReservationStatusOperations operations;
ReservationStatus(ReservationStatusOperations operations) {
this.operations = operations;
}
@Override
public ReservationStatus accept(Reservation reservation) {
return operations.accept(reservation);
}
@Override
public ReservationStatus charge(Reservation reservation) {
return operations.charge(reservation);
}
@Override
public ReservationStatus cancel(Reservation reservation) {
return operations.cancel(reservation);
}
}

這是我們的ReservationStatus enum的最終的原始碼(實現ReservationStatusOperations 不是必須的)。把事情變簡單:每一個enum值都自己特定的ReservationStatusOperations 實現(簡寫為Rso)。ReservationStatusOperations 的實現作為建構函式的引數,然後賦給一個命名為operations的final型別的域。現在,不管enum中的業務方法什麼時候被呼叫,呼叫將被委託給特定的ReservationStatusOperations 實現。

ReservationStatus.NEW.accept(reservation);       // will call NewRso.accept()
ReservationStatus.ACCEPTED.accept(reservation);  // will call AcceptedRso.accept()

最後一個要實現的部分是包含業務方法Reservation領域物件。

public void accept() {
setStatus(status.accept(this));
}
public void charge() {
setStatus(status.charge(this));
}
public void cancel() {
setStatus(status.cancel(this));
}
public void setStatus(ReservationStatus status) {
if (status != null && status != this.status) {
log.debug("Reservation#" + id + ": changing status from " +
this.status + " to " + status);
this.status = status;
}

這裡發生了什麼?當你在一個Reservation領域物件例項上呼叫任何業務方法,ReservationStatus  enum的某個值的相應的方法就會被呼叫。依據當前的狀態,一個不同的方法(不同ReservationStatusOperations 實現的)將會被呼叫。但是沒有switch-case 和if-else結構,僅僅使用了多型。例如,如果當status域指向ReservationStatus.ACCEPTED,你呼叫了charge()方法,AcceptedRso.charge() 將會被呼叫,消費者將會被要求付款,付款之後,Reservation狀態改變成PAID。

但是如果我們在同一個例項再次呼叫charge()會發生什麼?status域現在指向ReservationStatus.PAID,所以PaidRso.charge() 將會被執行,這將會丟擲一個業務錯誤(為一個已付款的Reservation付款是無效的)。沒有條件判斷的程式碼,我們實現了一個業務方法狀態敏感的領域物件。

我還沒有提到的一件事是如何從一個業務方法改變Reservation的狀態。這是與原始的GoF模式第二個不同的地方。我從業務方法簡單的返回一個新的狀態,而不是傳遞一個StateContext 例項給每一個狀態敏感的操作(像accept()或者charge()方法),這種方式經常被用來改變狀態。如果給定的狀態不是null而且與先前的狀態不同(setStatus方法中實現),Reservation物件將轉變為給定的狀態。讓我們看一下在AcceptedRso 物件中是如何工作的(Reservation物件在ReservationStatus.ACCEPTED 狀態,它的方法將要被執行)。

public class AcceptedRso implements ReservationStatusOperations {
@Override
public ReservationStatus accept(Reservation reservation) {
throw new UnsupportedStatusTransitionException("accept", ReservationStatus.ACCEPTED);
}
@Override
public ReservationStatus charge(Reservation reservation) {
//charge client's credit card
//send e-mail
//print ticket
return ReservationStatus.PAID;
}
@Override
public ReservationStatus cancel(Reservation reservation) {
//send cancellation e-mail
return ReservationStatus.CANCELLED;
}
}

在ACCEPTED 狀態的Reservation 可以通過上邊的類原始碼很容易地理解:當一個Reservation已經被accept時,試圖accept第二次將會跑出一個錯誤,收費將使用客戶的信用卡,列印給他一個票據然後傳送一個email等等。同時,付費操作將返回一個PAID狀態,這將使Reservation轉換成這個狀態。這意味著第二次呼叫charge將被不同的ReservationStatusOperations 實現處理(PaidRso),沒有條件判斷。

上邊是關於狀態模式的全部。如果你不相信這種設計模式,比較一下使用條件判斷的程式碼這種傳統的方式的工作量和容易出錯的程式碼。

我沒有展示所有的ReservationStatusOperations 的實現,但是如果你將在基於Java EE的String或者EJB中引入這種方式,你可能已經看到一個彌天大謊。我描述了每一個業務方法應該發生的事情,但是沒有提供具體的實現。我沒有的原因是因為我遇到了一個大問題:一個Reservation例項通過手工(用new)或者持久化框架像hibernate建立。 It uses statically created enum which creates manually ReservationStatusOperations implementations.沒有辦法去注入依賴,DAOs和service.對於這個類來說,它的整個生命週期都在spring或者ejb的容器管轄之外。實際上,有一個簡單有效的解決方案,使用Spring和AspectJ。但是耐心點,我將在下一封郵件中詳細的解釋,如何給應用增加一點領域驅動的味道。

譯者注:有兩張圖片沒有許可權上傳,可以檢視原文連結中的文章的圖片