- 對于大多數軟件項目,主要重點應該放在域和域邏輯上
- 復雜的領域設計應基于模型。
DDD促進了技術專家和領域專家之間的創造性合作,以迭代方式切入問題的概念核心。 請注意,沒有該領域專家的幫助,技術專家可能無法完全理解領域的復雜性,而領域專家在沒有技術專家幫助的情況下就無法實際應用其知識。
在許多情況下,領域模型對象封裝了內部狀態,本質上就是元素的歷史,即對象以有狀態方式運行。 在那種情況下,對象保持其私有狀態,這最終會影響其行為。 為了表示對象的狀態以及以干凈的方式處理其狀態轉換,可以使用狀態設計模式 。 簡而言之, 狀態模式可以解決如何使行為取決于狀態的問題。
顯然,DDD與狀態設計模式緊密相關。 我是DDD的新手,所以我將讓我們最好的JCG合作伙伴之一 Tomasz Nurkiewicz 通過使用State Design Pattern的示例向您介紹DDD 。
(注意:對原始帖子進行了少量編輯以提高可讀性)
許多企業應用程序中的某些領域對象都包含狀態的概念。 國家有兩個主要特征:
- 域對象的行為(其對業務方法的響應方式)取決于其狀態
- 業務方法可能會更改對象的狀態,從而迫使對象在調用特定方法后的行為有所不同。
如果您無法想象域對象狀態的任何真實示例,請考慮租賃公司中的Car實體。 小汽車在保留相同對象的同時,還有一個附加標志,稱為狀態,這對于公司至關重要。 狀態標志可以具有三個值:
- 可用的
- 出租
- 失蹤
顯然,目前無法租用處于RENTED或MISSING狀態的Car,并且rent()方法應該失敗。 但是,當汽車退回并且其狀態為AVAILABLE時,除了記住已租車的客戶外,在Car實例上調用rent()應該應該將汽車狀態更改為RENTED。 狀態標志(可能是數據庫中的單個字符或整數)是對象狀態的一個示例,因為它影響業務方法,反之亦然,業務方法可以更改它。
現在想一會兒,您將如何實現這種方案,我相信您已經在工作中見過很多次了。 您有許多業務方法,具體取決于當前狀態,也可能取決于多個狀態。 如果您喜歡面向對象的編程,則可能會立即考慮繼承并創建擴展Car的AvailableCar,RentedCar和MissingCar類。 它看起來不錯,但是非常不切實際,特別是當Car是一個持久對象時。 實際上,這種方法設計得不好:改變的不是整個對象,而是內部狀態的一部分–我們不是在替換對象,而只是在更改它。 也許您考慮過在每個方法中根據狀態執行不同任務的if-else-if-else級聯。 相信我,不要去那里,那是通往代碼維護地獄的道路。
取而代之的是,我們將使用繼承和多態性,但是要采用一種更為巧妙的方式:使用State GoF模式 。 例如,我選擇了一個名為Reservation的實體,該實體可以具有以下狀態之一:
生命周期流程很簡單:創建保留時,它具有NEW狀態(狀態)。 然后,一些授權人員可以接受預訂,例如導致臨時預訂座位,并向用戶發送一封電子郵件,要求他為預訂付款。 然后,當用戶執行匯款時,將進行入帳,打印票證并將第二封電子郵件發送給客戶。
當然,您知道某些動作的副作用取決于保留當前狀態。 例如,您可以隨時取消預訂,但是根據預訂狀態,這可能會導致退款和取消預訂,或者僅向用戶發送電子郵件。 此外,某些操作在特定狀態下(用戶將錢轉至已取消的預訂該怎么辦)毫無意義,或應被忽略。 現在想象一下,如果必須為每個狀態和每個方法使用if-else構造,那么編寫上面狀態機圖上公開的每個業務方法將有多么困難。
為了解決此問題,我將不解釋原始的GoF State設計模式。 相反,我將使用Java枚舉功能介紹這種模式的一些變化。 代替為狀態抽象創建抽象類/接口并為每個狀態編寫實現,我僅創建了一個包含所有可用狀態/狀態的枚舉:
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 / setter,或者也許我們可以只使用Groovy而忘記它們了?):
public class Reservation {private int id;private String name;private Calendar date;private BigDecimal price;private ReservationStatus status = ReservationStatus.NEW;//getters/setters}
如果Reservation是一個持久域對象,則其狀態(ReservationStatus)顯然也應該是持久的。 這種觀察將我們帶到了使用枚舉而不是抽象類的第一個重大優勢:JPA / Hibernate可以使用枚舉的名稱或序數值(默認情況下)輕松地序列化Java枚舉并將其保留在數據庫中。 在原始GoF模式中,我們寧愿將ReservationStatusOperations直接放在域對象中,并在狀態更改時切換實現。 我建議使用枚舉,僅更改枚舉值。 使用枚舉的另一個優點(以框架為中心,更不重要)是將所有可能的狀態都列在一個位置。 您無需搜尋源代碼即可搜索基狀態類的所有實現,所有內容都可以在一個逗號分隔的列表中看到。
好吧,深吸一口氣,現在我將解釋所有這些部分如何協同工作以及到底為什么ReservationStatusOperations中的業務操作返回ReservationStatus。 首先,您必須回顧實際的枚舉是什么。 它們不僅僅是像C / C ++中的單個名稱空間中的常量的集合。 在Java中,枚舉是一組封閉的類集,它們從一個通用的基類(例如ReservationStatus)繼承,而該基類又從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很誘人,但對于長期開發而言,這是一個壞主意。 不僅枚舉源代碼會很長(已實現方法的總數等于狀態數量乘以業務方法的數量),而且設計不好(單個類中所有狀態的業務邏輯)。 同樣,對于在過去兩周內未通過SCJP考試的任何人來說,實現與該語法的其余部分一起使用的接口的枚舉可能都是相反的。 相反,我們將提供一個簡單的間接級別,因為“ 計算機科學中的任何問題都可以通過另一層間接解決 ”。
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;}@Overridepublic ReservationStatus accept(Reservation reservation) {return operations.accept(reservation);}@Overridepublic ReservationStatus charge(Reservation reservation) {return operations.charge(reservation);}@Overridepublic ReservationStatus cancel(Reservation reservation) {return operations.cancel(reservation);}}
這是我們ReservationStatus枚舉的最終源代碼(無需實現ReservationStatusOperations)。 簡而言之:每個枚舉值都有其自己的ReservationStatusOperations(簡稱Rso)的不同實現。 此實現作為構造函數參數傳遞,并分配給名為operation的最終字段。 現在,每當在枚舉上調用業務方法時,它將被委派給該枚舉專用的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;}
這里會發生什么? 在保留域對象實例上調用任何業務方法時,將在ReservationStatus枚舉值上調用相應的方法。 根據當前狀態,將調用不同的方法(具有不同的ReservationStatusOperations實現)。 但是沒有切換用例或if-else構造,只有純多態性。 例如,如果您在狀態字段指向ReservationStatus.ACCEPTED,AcceptedRso.charge()的情況下調用charge(),則向預訂的客戶收取費用,并且預訂狀態更改為PAID。
但是,如果我們在同一實例上再次調用charge()會發生什么呢? status字段現在指向ReservationStatus.PAID,因此將執行PaidRso.charge(),這將引發業務異常(對已付費的預訂收取費用無效)。 在沒有條件代碼的情況下,我們使用對象本身包含的業務方法實現了狀態感知域對象。
我還沒有提到的一件事是如何從業務方法更改狀態。 這是與原始GoF模式的第二個區別。 我沒有將StateContext實例傳遞給每個可用于更改狀態的狀態感知操作(例如accept()或charge()),而是僅從業務方法返回新狀態。 如果狀態不為null,并且與前一個狀態不同(setStatus()方法),則保留將轉換為給定狀態。 讓我們看一下它如何在AcceptedRso對象上工作(當Reservation處于ReservationStatus.ACCEPTED狀態時,將執行其方法):
public class AcceptedRso implements ReservationStatusOperations {@Overridepublic ReservationStatus accept(Reservation reservation) {throw new UnsupportedStatusTransitionException("accept", ReservationStatus.ACCEPTED);}@Overridepublic ReservationStatus charge(Reservation reservation) {//charge client's credit card//send e-mail//print ticketreturn ReservationStatus.PAID;}@Overridepublic ReservationStatus cancel(Reservation reservation) {//send cancellation e-mailreturn ReservationStatus.CANCELLED;}}
僅需閱讀上面的課程,即可很容易地了解處于“已接受”狀態的預訂行為:第二次嘗試接受(已接受預訂時)將引發異常,收費將向客戶的信用卡收取費用,向其打印一張機票并發送電子郵件等。此外,收費會返回PAID狀態,這將導致預訂轉移到該狀態。 這意味著另一個對charge()的調用將由不同的ReservationStatusOperations實現(PaidRso)處理,沒有條件代碼。
這將全部與國家模式有關。 如果您對這種設計模式不滿意,請與使用條件代碼的經典方法進行比較,并比較工作量和出錯率。 還要考慮一會兒,添加新的狀態或與狀態有關的操作時需要什么,以及閱讀這樣的代碼有多容易。
我沒有顯示所有ReservationStatusOperations實現,但是如果您想在基于Spring或EJB的Java EE應用程序中引入這種方法,那么您可能會發現其中的一個大謊言。 我評論了每種業務方法中應發生的情況,但未提供實際的實現。 我沒有,因為我遇到了一個大問題:一個Reservation實例是手工創建的(使用新的)或由諸如Hibernate之類的持久性框架創建的。 它使用靜態創建的枚舉,該枚舉可手動創建ReservationStatusOperations實現。 無法將任何依賴項,DAO和服務注入此類,因為它們的生命周期是在Spring或EJB容器范圍之外進行控制的。 實際上,有一個使用Spring和AspectJ的簡單而強大的解決方案。 但是請耐心等待,我將在下一篇文章中詳細解釋它,為我們的應用程序添加一些域驅動的風格。
而已。 我們的JCG合作伙伴 Tomasz Nurkiewicz撰寫了一篇非常有趣的文章,介紹如何在DDD方法中利用狀態模式 。 我當然很期待本教程的下一部分,該教程將在幾天后在JavaCodeGeeks上托管。 更新:下一部分是使用Spring和AspectJ的域驅動設計 。
- Spring和AspectJ的領域驅動設計
- 零XML的Spring配置
- 正確記錄應用程序的10個技巧
- 每個程序員都應該知道的事情
- 依賴注入–手動方式
翻譯自: https://www.javacodegeeks.com/2011/02/state-pattern-domain-driven-design.html