在域驱动设计中使用状态模式
DDD促進(jìn)了技術(shù)專家和領(lǐng)域?qū)<抑g的創(chuàng)造性合作,以迭代方式切入問題的概念核心。 請注意,沒有該領(lǐng)域?qū)<业膸椭?#xff0c;技術(shù)專家可能無法完全理解領(lǐng)域的復(fù)雜性,而領(lǐng)域?qū)<以跊]有技術(shù)專家?guī)椭那闆r下就無法實際應(yīng)用其知識。
在許多情況下,領(lǐng)域模型對象封裝了內(nèi)部狀態(tài),本質(zhì)上就是元素的歷史,即對象以有狀態(tài)方式運行。 在那種情況下,對象保持其私有狀態(tài),這最終會影響其行為。 為了表示對象的狀態(tài)以及以干凈的方式處理其狀態(tài)轉(zhuǎn)換,可以使用狀態(tài)設(shè)計模式 。 簡而言之, 狀態(tài)模式可以解決如何使行為取決于狀態(tài)的問題。
顯然,DDD與狀態(tài)設(shè)計模式緊密相關(guān)。 我是DDD的新手,所以我將讓我們最好的JCG合作伙伴之一 Tomasz Nurkiewicz 通過使用State Design Pattern的示例向您介紹DDD 。
(注意:對原始帖子進(jìn)行了少量編輯以提高可讀性)
許多企業(yè)應(yīng)用程序中的某些領(lǐng)域?qū)ο蠖及瑺顟B(tài)的概念。 國家有兩個主要特征:
- 域?qū)ο蟮男袨?#xff08;其對業(yè)務(wù)方法的響應(yīng)方式)取決于其狀態(tài)
- 業(yè)務(wù)方法可能會更改對象的狀態(tài),從而迫使對象在調(diào)用特定方法后的行為有所不同。
如果您無法想象域?qū)ο鬆顟B(tài)的任何真實示例,請考慮租賃公司中的Car實體。 小汽車在保留相同對象的同時,還有一個附加標(biāo)志,稱為狀態(tài),這對于公司至關(guān)重要。 狀態(tài)標(biāo)志可以具有三個值:
顯然,目前無法租用處于RENTED或MISSING狀態(tài)的Car,并且rent()方法應(yīng)該失敗。 但是,當(dāng)汽車退回并且其狀態(tài)為AVAILABLE時,除了記住已租車的客戶外,在Car實例上調(diào)用rent()應(yīng)該應(yīng)該將汽車狀態(tài)更改為RENTED。 狀態(tài)標(biāo)志(可能是數(shù)據(jù)庫中的單個字符或整數(shù))是對象狀態(tài)的一個示例,因為它影響業(yè)務(wù)方法,反之亦然,業(yè)務(wù)方法可以更改它。
現(xiàn)在想一會兒,您將如何實現(xiàn)這種方案,我相信您已經(jīng)在工作中見過很多次了。 您有許多業(yè)務(wù)方法,具體取決于當(dāng)前狀態(tài),也可能取決于多個狀態(tài)。 如果您喜歡面向?qū)ο蟮木幊?#xff0c;則可能會立即考慮繼承并創(chuàng)建擴(kuò)展Car的AvailableCar,RentedCar和MissingCar類。 它看起來不錯,但是非常不切實際,特別是當(dāng)Car是一個持久對象時。 實際上,這種方法設(shè)計得不好:改變的不是整個對象,而是內(nèi)部狀態(tài)的一部分–我們不是在替換對象,而只是在更改它。 也許您考慮過在每個方法中根據(jù)狀態(tài)執(zhí)行不同任務(wù)的if-else-if-else級聯(lián)。 相信我,不要去那里,那是通往代碼維護(hù)地獄的道路。
取而代之的是,我們將使用繼承和多態(tài)性,但是要采用一種更為巧妙的方式:使用State GoF模式 。 例如,我選擇了一個名為Reservation的實體,該實體可以具有以下狀態(tài)之一:
生命周期流程很簡單:創(chuàng)建保留時,它具有NEW狀態(tài)(狀態(tài))。 然后,一些授權(quán)人員可以接受預(yù)訂,例如導(dǎo)致臨時預(yù)訂座位,并向用戶發(fā)送一封電子郵件,要求他為預(yù)訂付款。 然后,當(dāng)用戶執(zhí)行匯款時,將進(jìn)行入帳,打印票證并將第二封電子郵件發(fā)送給客戶。
當(dāng)然,您知道某些動作的副作用取決于保留當(dāng)前狀態(tài)。 例如,您可以隨時取消預(yù)訂,但是根據(jù)預(yù)訂狀態(tài),這可能會導(dǎo)致退款和取消預(yù)訂,或者僅向用戶發(fā)送電子郵件。 此外,某些操作在特定狀態(tài)下(用戶將錢轉(zhuǎn)至已取消的預(yù)訂該怎么辦)毫無意義,或應(yīng)被忽略。 現(xiàn)在想象一下,如果必須為每個狀態(tài)和每個方法使用if-else構(gòu)造,那么編寫上面狀態(tài)機(jī)圖上公開的每個業(yè)務(wù)方法將有多么困難。
為了解決此問題,我將不解釋原始的GoF State設(shè)計模式。 相反,我將使用Java枚舉功能介紹這種模式的一些變化。 代替為狀態(tài)抽象創(chuàng)建抽象類/接口并為每個狀態(tài)編寫實現(xiàn),我僅創(chuàng)建了一個包含所有可用狀態(tài)/狀態(tài)的枚舉:
public enum ReservationStatus {NEW,ACCEPTED,PAID,CANCELLED; }我還根據(jù)該狀態(tài)為所有業(yè)務(wù)方法創(chuàng)建了一個接口。 將此接口視為所有狀態(tài)的抽象基礎(chǔ),但是我們將以稍微不同的方式使用它:
public interface ReservationStatusOperations {ReservationStatus accept(Reservation reservation);ReservationStatus charge(Reservation reservation);ReservationStatus cancel(Reservation reservation); }最后是Reservation域?qū)ο?#xff0c;它恰好同時是一個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是一個持久域?qū)ο?#xff0c;則其狀態(tài)(ReservationStatus)顯然也應(yīng)該是持久的。 這種觀察將我們帶到了使用枚舉而不是抽象類的第一個重大優(yōu)勢:JPA / Hibernate可以使用枚舉的名稱或序數(shù)值(默認(rèn)情況下)輕松地序列化Java枚舉并將其保留在數(shù)據(jù)庫中。 在原始GoF模式中,我們寧愿將ReservationStatusOperations直接放在域?qū)ο笾?#xff0c;并在狀態(tài)更改時切換實現(xiàn)。 我建議使用枚舉,僅更改枚舉值。 使用枚舉的另一個優(yōu)點(以框架為中心,更不重要)是將所有可能的狀態(tài)都列在一個位置。 您無需搜尋源代碼即可搜索基狀態(tài)類的所有實現(xiàn),所有內(nèi)容都可以在一個逗號分隔的列表中看到。
好吧,深吸一口氣,現(xiàn)在我將解釋所有這些部分如何協(xié)同工作以及到底為什么ReservationStatusOperations中的業(yè)務(wù)操作返回ReservationStatus。 首先,您必須回顧實際的枚舉是什么。 它們不僅僅是像C / C ++中的單個名稱空間中的常量的集合。 在Java中,枚舉是一組封閉的類集,它們從一個通用的基類(例如ReservationStatus)繼承,而該基類又從Enum繼承。 因此,在使用枚舉時,我們可能會利用多態(tài)和繼承:
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很誘人,但對于長期開發(fā)而言,這是一個壞主意。 不僅枚舉源代碼會很長(已實現(xiàn)方法的總數(shù)等于狀態(tài)數(shù)量乘以業(yè)務(wù)方法的數(shù)量),而且設(shè)計不好(單個類中所有狀態(tài)的業(yè)務(wù)邏輯)。 同樣,對于在過去兩周內(nèi)未通過SCJP考試的任何人來說,實現(xiàn)與該語法的其余部分一起使用的接口的枚舉可能都是相反的。 相反,我們將提供一個簡單的間接級別,因為“ 計算機(jī)科學(xué)中的任何問題都可以通過另一層間接解決 ”。
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枚舉的最終源代碼(無需實現(xiàn)ReservationStatusOperations)。 簡而言之:每個枚舉值都有其自己的ReservationStatusOperations(簡稱Rso)的不同實現(xiàn)。 此實現(xiàn)作為構(gòu)造函數(shù)參數(shù)傳遞,并分配給名為operation的最終字段。 現(xiàn)在,每當(dāng)在枚舉上調(diào)用業(yè)務(wù)方法時,它將被委派給該枚舉專用的ReservationStatusOperations實現(xiàn):
ReservationStatus.NEW.accept(reservation); // will call NewRso.accept() ReservationStatus.ACCEPTED.accept(reservation); // will call AcceptedRso.accept()最后一個難題是Reservation域?qū)ο?#xff0c;包括業(yè)務(wù)方法:
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;}這里會發(fā)生什么? 在保留域?qū)ο髮嵗险{(diào)用任何業(yè)務(wù)方法時,將在ReservationStatus枚舉值上調(diào)用相應(yīng)的方法。 根據(jù)當(dāng)前狀態(tài),將調(diào)用不同的方法(具有不同的ReservationStatusOperations實現(xiàn))。 但是沒有切換用例或if-else構(gòu)造,只有純多態(tài)性。 例如,如果您在狀態(tài)字段指向ReservationStatus.ACCEPTED,AcceptedRso.charge()的情況下調(diào)用charge(),則向預(yù)訂的客戶收取費用,并且預(yù)訂狀態(tài)更改為PAID。
但是,如果我們在同一實例上再次調(diào)用charge()會發(fā)生什么呢? status字段現(xiàn)在指向ReservationStatus.PAID,因此將執(zhí)行PaidRso.charge(),這將引發(fā)業(yè)務(wù)異常(對已付費的預(yù)訂收取費用無效)。 在沒有條件代碼的情況下,我們使用對象本身包含的業(yè)務(wù)方法實現(xiàn)了狀態(tài)感知域?qū)ο蟆?
我還沒有提到的一件事是如何從業(yè)務(wù)方法更改狀態(tài)。 這是與原始GoF模式的第二個區(qū)別。 我沒有將StateContext實例傳遞給每個可用于更改狀態(tài)的狀態(tài)感知操作(例如accept()或charge()),而是僅從業(yè)務(wù)方法返回新狀態(tài)。 如果狀態(tài)不為null,并且與前一個狀態(tài)不同(setStatus()方法),則保留將轉(zhuǎn)換為給定狀態(tài)。 讓我們看一下它如何在AcceptedRso對象上工作(當(dāng)Reservation處于ReservationStatus.ACCEPTED狀態(tài)時,將執(zhí)行其方法):
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;}}僅需閱讀上面的課程,即可很容易地了解處于“已接受”狀態(tài)的預(yù)訂行為:第二次嘗試接受(已接受預(yù)訂時)將引發(fā)異常,收費將向客戶的信用卡收取費用,向其打印一張機(jī)票并發(fā)送電子郵件等。此外,收費會返回PAID狀態(tài),這將導(dǎo)致預(yù)訂轉(zhuǎn)移到該狀態(tài)。 這意味著另一個對charge()的調(diào)用將由不同的ReservationStatusOperations實現(xiàn)(PaidRso)處理,沒有條件代碼。
這將全部與國家模式有關(guān)。 如果您對這種設(shè)計模式不滿意,請與使用條件代碼的經(jīng)典方法進(jìn)行比較,并比較工作量和出錯率。 還要考慮一會兒,添加新的狀態(tài)或與狀態(tài)有關(guān)的操作時需要什么,以及閱讀這樣的代碼有多容易。
我沒有顯示所有ReservationStatusOperations實現(xiàn),但是如果您想在基于Spring或EJB的Java EE應(yīng)用程序中引入這種方法,那么您可能會發(fā)現(xiàn)其中的一個大謊言。 我評論了每種業(yè)務(wù)方法中應(yīng)發(fā)生的情況,但未提供實際的實現(xiàn)。 我沒有,因為我遇到了一個大問題:一個Reservation實例是手工創(chuàng)建的(使用新的)或由諸如Hibernate之類的持久性框架創(chuàng)建的。 它使用靜態(tài)創(chuàng)建的枚舉,該枚舉可手動創(chuàng)建ReservationStatusOperations實現(xiàn)。 無法將任何依賴項,DAO和服務(wù)注入此類,因為它們的生命周期是在Spring或EJB容器范圍之外進(jìn)行控制的。 實際上,有一個使用Spring和AspectJ的簡單而強(qiáng)大的解決方案。 但是請耐心等待,我將在下一篇文章中詳細(xì)解釋它,為我們的應(yīng)用程序添加一些域驅(qū)動的風(fēng)格。
而已。 我們的JCG合作伙伴 Tomasz Nurkiewicz撰寫了一篇非常有趣的文章,介紹如何在DDD方法中利用狀態(tài)模式 。 我當(dāng)然很期待本教程的下一部分,該教程將在幾天后在JavaCodeGeeks上托管。 更新:下一部分是使用Spring和AspectJ的域驅(qū)動設(shè)計 。
相關(guān)文章 :- Spring和AspectJ的領(lǐng)域驅(qū)動設(shè)計
- 零XML的Spring配置
- 正確記錄應(yīng)用程序的10個技巧
- 每個程序員都應(yīng)該知道的事情
- 依賴注入–手動方式
翻譯自: https://www.javacodegeeks.com/2011/02/state-pattern-domain-driven-design.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的在域驱动设计中使用状态模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 赛车电影《GT 赛车:极速狂飙》今日内地
- 下一篇: Google Guava库必需品