閱讀804 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

領域驅動設計是軟件開發的一種方式,問題複雜的地方通過將具體實現和一個不斷改進的核心業務概念的模型連接解決。這個概念是Eric Evans提出的,https://www.domaindrivendesign.org/這個網站來促進領域驅動設計的使用。關於領域驅動設計的定義,https://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。

1 public enum ReservationStatus {
2 NEW,
3 ACCEPTED,
4 PAID,
5 CANCELLED;
6 }

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

1 public interface ReservationStatusOperations {
2 ReservationStatus accept(Reservation reservation);
3 ReservationStatus charge(Reservation reservation);
4 ReservationStatus cancel(Reservation reservation);
5 }

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

1 public class Reservation {
2 private int id;
3 private String name;
4 private Calendar date;
5 private BigDecimal price;
6 private ReservationStatus status = ReservationStatus.NEW;
7 //getters/setters
8 }

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

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

01 public enum ReservationStatus implements ReservationStatusOperations {
02 NEW {
03 public ReservationStatus accept(Reservation reservation) {
04 //..
05 }
06 public ReservationStatus charge(Reservation reservation) {
07 //..
08 }
09 public ReservationStatus cancel(Reservation reservation) {
10 //..
11 }
12 },
13 ACCEPTED {
14 public ReservationStatus accept(Reservation reservation) {
15 //..
16 }
17 public ReservationStatus charge(Reservation reservation) {
18 //..
19 }
20 public ReservationStatus cancel(Reservation reservation) {
21 //..
22 }
23 },
24 PAID {/*...*/},
25 CANCELLED {/*...*/};
26 }

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

01 public enum ReservationStatus implements ReservationStatusOperations {
02 NEW(new NewRso()),
03 ACCEPTED(new AcceptedRso()),
04 PAID(new PaidRso()),
05 CANCELLED(new CancelledRso());
06 private final ReservationStatusOperations operations;
07 ReservationStatus(ReservationStatusOperations operations) {
08 this.operations = operations;
09 }
10 @Override
11 public ReservationStatus accept(Reservation reservation) {
12 return operations.accept(reservation);
13 }
14 @Override
15 public ReservationStatus charge(Reservation reservation) {
16 return operations.charge(reservation);
17 }
18 @Override
19 public ReservationStatus cancel(Reservation reservation) {
20 return operations.cancel(reservation);
21 }
22 }

這是我們的ReservationStatus enum的最終的源代碼(實現ReservationStatusOperations 不是必須的)。把事情變簡單:每一個enum值都自己特定的ReservationStatusOperations 實現(簡寫為Rso)。ReservationStatusOperations 的實現作為構造函數的參數,然後賦給一個命名為operations的final類型的域。現在,不管enum中的業務方法什麼時候被調用,調用將被委托給特定的ReservationStatusOperations 實現。

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

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

01 public void accept() {
02 setStatus(status.accept(this));
03 }
04 public void charge() {
05 setStatus(status.charge(this));
06 }
07 public void cancel() {
08 setStatus(status.cancel(this));
09 }
10 public void setStatus(ReservationStatus status) {
11 if (status != null && status != this.status) {
12 log.debug("Reservation#" + id + ": changing status from " +
13 this.status + " to " + status);
14 this.status = status;
15 }

這裏發生了什麼?當你在一個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 狀態,它的方法將要被執行)。

01 public class AcceptedRso implements ReservationStatusOperations {
02 @Override
03 public ReservationStatus accept(Reservation reservation) {
04 throw new UnsupportedStatusTransitionException("accept", ReservationStatus.ACCEPTED);
05 }
06 @Override
07 public ReservationStatus charge(Reservation reservation) {
08 //charge client's credit card
09 //send e-mail
10 //print ticket
11 return ReservationStatus.PAID;
12 }
13 @Override
14 public ReservationStatus cancel(Reservation reservation) {
15 //send cancellation e-mail
16 return ReservationStatus.CANCELLED;
17 }
18 }

在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。但是耐心點,我將在下一封郵件中詳細的解釋,如何給應用增加一點領域驅動的味道。

就這樣。一個非常有趣的郵件,解釋了如何在DDD方式中使用狀態模式,作者是我們的JCG夥伴,Tomasz Nurkiewicz.。我非常期待這個教程的下一個部分。下一個部分是:Domain Driven Design with Spring and AspectJ.

譯者注:有兩張圖片沒有權限上傳,可以查看原文鏈接中的文章的圖片

Related Articles :
Domain Driven Design with Spring and AspectJ
Spring configuration with zero XML
10 Tips for Proper Application Logging
Things Every Programmer Should Know
Dependency Injection – The manual way

最後更新:2017-05-23 11:02:52

  上一篇:go  Java IO: 其他字符流(下)
  下一篇:go  深入理解Java內存模型(一)——基礎