领域驱动设计模式设计与实践_在域驱动设计中使用状态模式
領(lǐng)域驅(qū)動設(shè)計模式設(shè)計與實(shí)踐
域驅(qū)動設(shè)計(DDD)是一種開發(fā)軟件的方法,其中,通過將實(shí)現(xiàn)與核心業(yè)務(wù)概念的不斷發(fā)展的模型相聯(lián)系,解決了問題的復(fù)雜性。 該術(shù)語是由Eric Evans創(chuàng)造的,并且有一個DDD專用站點(diǎn)可以促進(jìn)其使用。 根據(jù)其定義( “域驅(qū)動設(shè)計術(shù)語表” ),DDD是一種軟件開發(fā)方法,它建議:DDD促進(jìn)了技術(shù)專家和領(lǐng)域?qū)<抑g的創(chuàng)造性合作,以迭代方式切入問題的概念核心。 請注意,沒有該領(lǐng)域?qū)<业膸椭?#xff0c;技術(shù)專家可能無法完全理解領(lǐng)域的復(fù)雜性,而領(lǐng)域?qū)<以跊]有技術(shù)專家?guī)椭那闆r下就無法實(shí)際應(yīng)用其知識。
在許多情況下,領(lǐng)域模型對象封裝了內(nèi)部狀態(tài),本質(zhì)上就是內(nèi)部元素的歷史,即對象以有狀態(tài)方式運(yùn)行。 在這種情況下,對象保持其私有狀態(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)的任何現(xiàn)實(shí)示例,請考慮租賃公司中的Car實(shí)體。 小汽車在保留相同對象的同時,還有一個附加標(biāo)志,稱為狀態(tài),這對于公司至關(guān)重要。 狀態(tài)標(biāo)志可以具有三個值:
顯然,目前無法租用處于RENTED或MISSING狀態(tài)的Car,并且rent()方法應(yīng)該失敗。 但是,當(dāng)汽車退回并且其狀態(tài)為AVAILABLE時,除了記住已租車的客戶外,在Car實(shí)例上調(diào)用rent()應(yīng)該應(yīng)該將汽車狀態(tài)更改為RENTED。 狀態(tài)標(biāo)志(可能是數(shù)據(jù)庫中的單個字符或整數(shù))是對象狀態(tài)的一個示例,因?yàn)樗绊憳I(yè)務(wù)方法,反之亦然,業(yè)務(wù)方法可以更改它。
現(xiàn)在想一會兒,您將如何實(shí)現(xiàn)這種方案?我敢肯定,您已經(jīng)在工作中見過很多次了。 您有許多業(yè)務(wù)方法,具體取決于當(dāng)前狀態(tài),也可能取決于多個狀態(tài)。 如果您喜歡面向?qū)ο蟮木幊?#xff0c;則可能會立即考慮繼承并創(chuàng)建擴(kuò)展Car的AvailableCar,RentedCar和MissingCar類。 它看起來不錯,但是非常不切實(shí)際,特別是當(dāng)Car是一個持久對象時。 實(shí)際上,這種方法設(shè)計得不好:更改的不是整個對象,而是內(nèi)部狀態(tài)的一部分–我們不是在替換對象,而只是在更改它。 也許您考慮過if-else-if-else級聯(lián)…在每種方法中根據(jù)狀態(tài)執(zhí)行不同的任務(wù)。 相信我,不要去那里,這是通往代碼維護(hù)地獄的道路。
取而代之的是,我們將使用繼承和多態(tài)性,但是要采用一種更為巧妙的方式:使用State GoF模式 。 作為示例,我選擇了一個名為Reservation的實(shí)體,該實(shí)體可以具有以下狀態(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)編寫實(shí)現(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實(shí)體(省略了getters / setters,或者也許我們可以只使用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)更改時切換實(shí)現(xiàn)。 我建議使用枚舉,僅更改枚舉值。 使用枚舉的另一個優(yōu)點(diǎn)(以框架為中心,更不重要)是將所有可能的狀態(tài)都列在一個位置。 您無需搜尋源代碼即可搜索基狀態(tài)類的所有實(shí)現(xiàn),所有內(nèi)容都可以在一個逗號分隔的列表中看到。
好吧,深吸一口氣,現(xiàn)在我將解釋所有這些部分如何協(xié)同工作以及到底為什么ReservationStatusOperations中的業(yè)務(wù)操作返回ReservationStatus。 首先,您必須回顧實(shí)際的枚舉是什么。 它們不僅僅是像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ā)而言,這是一個壞主意。 不僅枚舉源代碼會很長(已實(shí)現(xiàn)方法的總數(shù)等于狀態(tài)數(shù)乘以業(yè)務(wù)方法的數(shù)量),而且設(shè)計不好(單個類中所有狀態(tài)的業(yè)務(wù)邏輯)。 此外,對于在過去兩周內(nèi)未通過SCJP考試的任何人來說,實(shí)現(xiàn)與該語法的其余部分一起使用的接口的枚舉可能會違反直覺。 相反,我們將提供一個簡單的間接級別,因?yà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枚舉的最終源代碼(無需實(shí)現(xiàn)ReservationStatusOperations)。 簡而言之:每個枚舉值都有其自己的ReservationStatusOperations(簡稱Rso)的不同實(shí)現(xiàn)。 此實(shí)現(xiàn)作為構(gòu)造函數(shù)參數(shù)傳遞,并分配給名為operation的最終字段。 現(xiàn)在,每當(dāng)在枚舉上調(diào)用業(yè)務(wù)方法時,它將被委派給該枚舉專用的ReservationStatusOperations實(shí)現(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ū)ο髮?shí)例上調(diào)用任何業(yè)務(wù)方法時,將在ReservationStatus枚舉值上調(diào)用相應(yīng)的方法。 根據(jù)當(dāng)前狀態(tài),將調(diào)用不同的方法(具有不同的ReservationStatusOperations實(shí)現(xiàn))。 但是沒有切換用例或if-else構(gòu)造,只有純多態(tài)性。 例如,如果您在狀態(tài)字段指向ReservationStatus.ACCEPTED,AcceptedRso.charge()的情況下調(diào)用charge(),則向預(yù)訂的客戶收取費(fèi)用,并且預(yù)訂狀態(tài)更改為PAID。
但是,如果我們在同一實(shí)例上再次調(diào)用charge()會發(fā)生什么呢? 狀態(tài)字段現(xiàn)在指向ReservationStatus.PAID,因此將執(zhí)行PaidRso.charge(),這將引發(fā)業(yè)務(wù)異常(對已付費(fèi)的預(yù)訂收取費(fèi)用無效)。 在沒有條件代碼的情況下,我們使用對象本身包含的業(yè)務(wù)方法實(shí)現(xiàn)了狀態(tài)感知域?qū)ο蟆?
我還沒有提到的一件事是如何從業(yè)務(wù)方法更改狀態(tài)。 這是與原始GoF模式的第二個區(qū)別。 與其將StateContext實(shí)例傳遞給可用于更改狀態(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ā)異常,收費(fèi)將向客戶的信用卡收取費(fèi)用,向其打印一張機(jī)票并發(fā)送電子郵件等。此外,收費(fèi)會返回PAID狀態(tài),這將導(dǎo)致預(yù)訂轉(zhuǎn)移到該狀態(tài)。 這意味著另一個對charge()的調(diào)用將由不同的ReservationStatusOperations實(shí)現(xiàn)(PaidRso)處理,沒有條件代碼。
這將與國家模式有關(guān)。 如果您對這種設(shè)計模式不滿意,請與使用條件代碼的經(jīng)典方法進(jìn)行比較,并比較工作量和出錯率。 還要考慮一會兒,添加新的狀態(tài)或與狀態(tài)相關(guān)的操作時需要什么,以及閱讀這樣的代碼有多容易。
我沒有展示所有的ReservationStatusOperations實(shí)現(xiàn),但是如果您想在基于Spring或EJB的Java EE應(yīng)用程序中引入這種方法,那么您可能已經(jīng)發(fā)現(xiàn)了一個很大的謊言。 我評論了每種業(yè)務(wù)方法中應(yīng)發(fā)生的情況,但未提供實(shí)際的實(shí)現(xiàn)。 我沒有,因?yàn)槲矣龅搅艘粋€大問題:一個Reservation實(shí)例是手工創(chuàng)建的(使用新的)或由一個像Hibernate這樣的持久性框架創(chuàng)建的。 它使用靜態(tài)創(chuàng)建的枚舉,該枚舉可手動創(chuàng)建ReservationStatusOperations實(shí)現(xiàn)。 無法將任何依賴項(xiàng),DAO和服務(wù)注入此類,因?yàn)樗鼈兊纳芷谑窃赟pring或EJB容器范圍之外進(jìn)行控制的。 實(shí)際上,有一個使用Spring和AspectJ的簡單而強(qiáng)大的解決方案。 但是請耐心等待,我將在下一篇文章中詳細(xì)解釋它,為我們的應(yīng)用程序添加一些域驅(qū)動的味道。
而已。 我們的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
領(lǐng)域驅(qū)動設(shè)計模式設(shè)計與實(shí)踐
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的领域驱动设计模式设计与实践_在域驱动设计中使用状态模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 西藏资质备案委托人只有一个吗?(西藏资质
- 下一篇: 如何在2020年保护你的网站-Astra