Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
九、提交訂單頁面顯示功能
19.1在templates/cart.html文件中設置表單提交:
19.2在order/urls.py中配置對應的url:
19.3在order/view.py中編寫OrderPlaceView類顯示訂單頁面:
19.4在templates/place_order.html文件中完成訂單頁面的顯示:
19.5在templates/order_place.html文件中完成創建訂單功能:
19.6在order/urls.py文件中定義提交訂單的url:
19.7數據庫MySQL的事務:
19.7.1事務概念: 一組mysql語句,要么執行,要么全不不執行。
19.7.2事務特點:
1、原子性:一組事務,要么成功;要么撤回。
2、穩定性 :有非法數據(外鍵約束之類),事務撤回。
3、隔離性:事務獨立運行。一個事務處理后的結果,影響了其他事務,那么其他事務會撤回。事務的100%隔離,需要犧牲速度。
4、可靠性:軟、硬件崩潰后,InnoDB數據表驅動會利用日志文件重構修改。可靠性和高速度不可兼得, innodb_flush_log_at_trx_commit 選項 決定什么時候吧事務保存到日志里。
19.7.3事務控制語句:
COMMIT;COMMIT會提交事務,并使已對數據庫進行的所有修改稱為永久性的;
ROLLBACK;回滾會結束用戶的務,并撤銷正在進行的所有未提交的修改;
SAVEPOINT identifier;SAVEPOINT允許在事務中創建一個保存點,一個事務中可以有多個SAVEPOINT;
RELEASE SAVEPOINT identifier;刪除一個事務的保存點,當沒有指定的保存點時,執行該語句會 拋出一個異常;
ROLLBACK TO identifier;把事務回滾到標記點;
19.7.4事務隔離級別:
Read Uncommitted(讀取未提交內容):在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用于實際應用,因為它的性能也不比其他級別好多少。讀取未提交的數據,也被稱之為臟讀(Dirty Read)。
Read Committed(讀取提交內容):這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別 也支持所謂的不可重復讀(Nonrepeatable Read),因為同一事務的其他實例在該實例處理其間可能會有新的commit,所以同一select可能返回不同結果。
Repeatable Read(可重讀):這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在并發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。
Serializable(可串行化):這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。
19.8提交訂單功能的業務分析:
19.9訂單并發處理:
悲觀鎖:獲取數據時對數據行了鎖定,其他事務要想獲取鎖,必須等原事務結束。
使用示例:select * from df_goods_sku where id=17 for update;
樂觀鎖:查詢時不鎖數據,提交更改時進行判斷.
使用示例:update df_goods_sku set stock=0, sales=1 where id=17 and stock=1;
19.9.1項目使用原則:
沖突比較少的時候,使用樂觀鎖。
沖突比較多的時候,使用悲觀鎖。
19.10在order/views.py中定義OrderCommitView訂單創建類:
二十、用戶中心的訂單頁面顯示
20.1在user/urls.py中定義用戶中心訂單頁的url:
20.2在user/view.py中定義UserOrderView類:
20.3在templates/user_center_order.html中顯示數據:
天的企業應用程序無疑是復雜的,并依賴一些專門技術(持久性,AJAX,Web服務等)來完成它們的工作。作為開發人員,我們傾向于關注這些技術細節是可以理解的。但事實是,一個不能解決業務需求的系統對任何人都沒有用,無論它看起來多么漂亮或者如何很好地構建其基礎設施。
領域驅動設計(DDD)的理念 - 首先由Eric Evans在他的同名書[1]中描述 - 是關于將我們的注意力放在應用程序的核心,關注業務領域固有的復雜性本身。我們還將核心域(業務獨有)與支持子域(通常是通用的,如金錢或時間)區分開來,并將更多的設計工作放在核心上。
域驅動設計包含一組用于從域模型構建企業應用程序的模式。在您的軟件生涯中,您可能已經遇到過許多這樣的想法,特別是如果您是OO語言的經驗豐富的開發人員。但將它們一起應用將允許您構建真正滿足業務需求的系統。
代碼和模型......
使用DDD,我們希望創建問題域的模型。持久性,用戶界面和消息傳遞的東西可以在以后出現,這是需要理解的領域,因為正在構建的系統中,可以區分公司的業務與競爭對手。 (如果不是這樣,那么考慮購買包裝產品)。
按模型,我們不是指圖表或一組圖表;確定,圖表很有用,但它們不是模型,只是模型的不同視圖(參見圖)。不,模型是我們選擇在軟件中實現的概念集,以代碼和用于構建交付系統的任何其他軟件工件表示。換句話說,代碼就是模型。文本編輯器提供了一種使用此模型的方法,盡管現代工具也提供了大量其他可視化(UML類圖,實體關系圖,Spring beandocs [2],Struts / JSF流等)。
Figure 1: Model vs Views of the Model
這是DDD模式的第一個:模型驅動設計(model-driven design)。這意味著能夠將模型中的概念映射到設計/代碼的概念(理想情況下)。模型的變化意味著代碼的變化;更改代碼意味著模型已更改。 DDD并沒有強制要求您使用面向對象來構建域 - 例如,我們可以使用規則引擎構建模型 - 但鑒于主流企業編程語言是基于OO的,大多數模型本質上都是OO。畢竟,OO基于建模范例。模型的概念將表示為類和接口,作為類成員的職責。
語言
現在讓我們看一下域驅動設計的另一個基本原則。回顧一下:我們想要構建一個捕獲正在構建的系統的問題域的域模型,并且我們將在代碼/軟件工件中表達這種理解。為了幫助我們做到這一點,DDD提倡領域專家和開發人員有意識地使用模型中的概念進行溝通。因此,域專家不會根據屏幕或菜單項上的字段描述新的用戶故事,而是討論域對象所需的基礎屬性或行為。類似地,開發人員不會討論數據庫表中的類或列的新實例變量。
嚴格要求我們開發一種普世的語言(ubiquitous language)。如果一個想法不能輕易表達,那么它表明了一個概念,這個概念在領域模型中缺失,并且團隊共同努力找出缺失的概念是什么。一旦建立了這個,那么數據庫表中的屏幕或列上的新字段就會繼續顯示。
像DDD一樣,這種開發無處不在的語言的想法并不是一個新想法:XPers稱之為“名稱系統”,多年來DBA將數據字典組合在一起。但無處不在的語言是一個令人回味的術語,可以出售給商業和技術人員。現在,“整個團隊”敏捷實踐正在成為主流,這也很有意義。
模型和上下文......
每當我們討論模型時,它總是在某種情況下。通常可以從使用該系統的最終用戶集推斷出該上下文。因此,我們有一個部署到交易員的前臺交易系統,或超市收銀員使用的銷售點系統。這些用戶以特定方式與模型的概念相關,并且模型的術語對這些用戶有意義,但不一定對該上下文之外的任何其他人有意義。 DDD稱之為有界上下文(BC)。每個域模型都只存在于一個BC中,而BC只包含一個域模型。
我必須承認,當我第一次讀到關于BC時,我看不出這一點:如果BC與域模型同構,為什么要引入一個新術語?如果只有與BC相互作用的最終用戶,則可能不需要這個術語。然而,不同的系統(BC)也相互交互,發送文件,傳遞消息,調用API等。如果我們知道有兩個BC相互交互,那么我們知道我們必須注意在一個概念之間進行轉換。領域和其他領域。
在模型周圍設置明確的邊界也意味著我們可以開始討論這些BC之間的關系。實際上,DDD確定了BC之間的一整套關系,因此當我們需要將不同的BC鏈接在一起時,我們可以合理地確定應該做什么:
已發布的語言:交互式BCs就共同的語言(例如企業服務總線上的一堆XML模式)達成一致,通過它們可以相互交互;
開放主機服務:BC指定任何其他BC可以使用其服務的協議(例如RESTful Web服務);
共享內核:兩個BC使用一個共同的代碼內核(例如一個庫)作為一個通用的通用語言,但是否則以他們自己的特定方式執行其他的東西;
客戶/供應商:一個BC使用另一個BC的服務,并且是另一個BC的利益相關者(客戶)。因此,它可以影響該BC提供的服務;
順從者:一個BC使用另一個BC的服務,但不是其他BC的利益相關者。因此,它使用“原樣”(符合)BC提供的協議或API;
反腐蝕層:一個BC使用另一個服務而不是利益相關者,但旨在通過引入一組適配器 - 一個反腐敗層來最小化它所依賴的BC變化的影響。
你可以看到,在這個列表中,兩個BC之間的合作水平逐漸降低(見圖2)。使用已發布的語言(published language),我們從BC建立一個他們可以互動的共同標準開始;既不擁有這種語言,而是由他們所居住的企業所擁有(甚至可能是行業標準)。有了開放主機服務(open host),我們仍然做得很好; BC提供其作為任何其他BC調用的運行時服務的功能,但是(可能)隨著服務的發展將保持向后兼容性。
Figure 2: Spectrum of Bounded Context Relationship
然而,當我們走向順從時,我們只是和我們一起生活; 一個BC明顯屈服于另一個。 如果我們必須與購買megabucks的總分類帳系統集成,那可能就是我們所處的情況。如果我們使用反腐敗層,那么我們通常會與遺留系統集成,但是 額外的層將我們盡可能地隔離開來。 當然,這需要花錢來實施,但它降低了依賴風險。 反腐敗層也比重新實現遺留系統便宜很多,這最多會分散我們對核心域的注意力,最壞的情況是以失敗告終。
DDD建議我們制定一個上下文圖(context map t)來識別我們的BC以及我們依賴或依賴的BC,以確定這些依賴關系的性質。 圖3顯示了我過去5年左右一直在研究的系統的上下文映射。
Figure 3: Context Mapping Example
所有這些關于背景圖和BC的討論有時被稱為戰略性DDD( strategic DDD),并且有充分的理由。 畢竟,當你想到它時,弄清楚BC之間的關系是非常政治的:我的系統將依賴哪些上游系統,我是否容易與它們集成,我是否能夠利用它們,我相信它們嗎? 下游也是如此:哪些系統將使用我的服務,我如何將我的功能作為服務公開,他們會對我有利嗎? 誤解了這一點,您的應用程序可能很容易失敗。
層和六邊形
現在讓我們轉向內部并考慮我們自己的BC(系統)的架構。 從根本上說,DDD只關心域層,實際上,它對其他層有很多話要說:表示,應用程序或基礎架構(或持久層)。 但它確實期望它們存在。 這是分層架構模式(圖4)。
Figure 4: Layered Architecture
當然,我們多年來一直在構建多層系統,但這并不意味著我們必須擅長它。確實,過去的一些主流技術 - 是的,EJB 2,我正在看著你! - 對域模型可以作為有意義的層存在的想法產生了積極的影響。所有的業務邏輯似乎滲透到應用層或(更糟糕的)表示層,留下一組貧血的域類[3]作為數據持有者的空殼。這不是DDD的意思。
因此,要絕對清楚,應用程序層中不應存在任何域邏輯。相反,應用程序層負責事務管理和安全性等事務。在某些體系結構中,它還可能負責確保從基礎結構/持久層中檢索的域對象在與之交互之前已正確初始化(盡管我更喜歡基礎結構層執行此操作)。
在表示層在單獨的存儲空間中運行的情況下,應用層也充當表示層和域層之間的中介。表示層通常處理域對象或域對象(數據傳輸對象或DTO)的可序列化表示,通常每個“視圖”一個。如果這些被修改,那么表示層會將任何更改發送回應用程序層,而應用程序層又確定已修改的域對象,從持久層加載它們,然后轉發對這些域對象的更改。
分層體系結構的一個缺點是它建議從表示層一直到基礎結構層的依賴性的線性堆疊。但是,我們可能希望在表示層和基礎結構層中支持不同的實現。如果(正如我認為的那樣!)我們想要測試我們的應用程序就是這種情況:
例如,FitNesse [4]等工具允許我們從最終用戶的角度驗證我們系統的行為。但是這些工具通常不會通過表示層,而是直接進入下一層,即應用層。所以從某種意義上說,FitNesse就是另一種觀察者。
同樣,我們可能有多個持久性實現。我們的生產實現可能使用RDBMS或類似技術,但是對于測試和原型設計,我們可能有一個輕量級實現(甚至可能在內存中),因此我們可以模擬持久性。
我們可能還想區分“內部”和“外部”層之間的交互,其中內部我指的是兩個層完全在我們的系統(或BC)內的交互,而外部交互跨越BC。
因此,不要將我們的應用程序視為一組圖層,另一種方法是將其視為六邊形[5],如圖5所示。我們的最終用戶使用的查看器以及FitNesse測試使用內部客戶端API(或端口),而來自其他BC的調用(例如,RESTful用于開放主機交互,或來自ESB適配器的調用用于已發布的語言交互)命中外部客戶端端口。對于后端基礎架構層,我們可以看到用于替代對象存儲實現的持久性端口,此外,域層中的對象可以通過外部服務端口調用其他BC。
Figure 5: Hexagonal Architecture
但這足夠大的東西; 讓我們來看看DDD在煤炭面板上的樣子。
構建模塊
正如我們已經注意到的,大多數DDD系統可能會使用OO范例。因此,我們的域對象的許多構建塊可能很熟悉,例如實體,值對象和模塊(entities, value objects and modules. )。例如,如果您是Java程序員,那么將DDD實體視為與JPA實體基本相同(使用@Entity注釋)就足夠安全了;值對象是字符串,數字和日期之類的東西;一個模塊就是一個包。
但是,DDD傾向于更多地強調值對象(value objects ),而不是過去習慣。所以,是的,您可以使用String來保存Customer的givenName屬性的值,例如,這可能是合理的。但是一筆錢,例如產品的價格呢?我們可以使用int或double,但是(甚至忽略可能的舍入錯誤)1或1.0是什么意思? $ 1嗎? 1? ¥1? 1分,甚至?相反,我們應該引入一個Money值類型,它封裝了Currency和任何舍入規則(將特定于Currency)。
而且,值對象應該是不可變的,并且應該提供一組無副作用的函數來操作它們。我們應該寫:
Money m1=new Money("GBP", 10);
Money m2=new Money("GBP", 20);
Money m3=m1.add(m2);
將m2添加到m1不會改變m1,而是返回一個新的Money對象(由m3引用),它表示一起添加的兩個Money。
值也應該具有值語義,這意味著(例如在Java和C#中)它們實現equals()和hashCode()。它們通常也可以序列化,可以是字節流,也可以是String格式。當我們需要堅持它們時,這很有用。
值對象常見的另一種情況是標識符。因此,(US)SocialSecurityNumber將是一個很好的例子,車輛的RegistrationNumber也是如此。 URL也是如此。因為我們已經重寫了equals()和hashCode(),所以這些都可以安全地用作哈希映射中的鍵。
引入價值對象不僅擴展了我們無處不在的語言,還意味著我們可以將行為推向價值觀本身。因此,如果我們確定Money永遠不會包含負值,我們可以在Money內部實現此檢查,而不是在使用Money的任何地方。如果SocialSecurityNumber具有校驗和數字(在某些國家/地區就是這種情況),則該校驗和的驗證可以在值對象中。我們可以要求URL驗證其格式,返回其方案(例如http),或者確定相對于其他URL的資源位置。
我們的另外兩個構建塊可能需要更少的解釋。實體通常是持久的,通常是可變的并且(因此)傾向于具有一生的狀態變化。在許多體系結構中,實體將作為行保存在數據庫表中。同時,模塊(包或命名空間)是確保域模型保持解耦的關鍵,并且不會成為泥漿中的一大塊[6]。在他的書中,埃文斯談到概念輪廓,這是一個優雅的短語,用于描述如何區分域的主要關注領域。模塊是實現這種分離的主要方式,以及確保模塊依賴性嚴格非循環的接口。我們使用諸如Uncle“Bob”Martin的依賴倒置原則[7]之類的技術來確保依賴關系是嚴格單向的。
實體,值和模塊是核心構建塊,但DDD還有一些不太熟悉的構建塊。我們現在來看看這些。
聚合和聚合根
如果您精通UML,那么您將記住,它允許我們將兩個對象之間的關聯建模為簡單關聯,聚合或使用組合。聚合根(有時縮寫為AR)是通過組合組成其他實體(以及它自己的值)的實體。也就是說,聚合實體僅由根引用(可能是可傳遞的),并且可能不會被聚合外的任何對象(永久地)引用。換句話說,如果實體具有對另一個實體的引用,則引用的實體必須位于同一聚合內,或者是某個其他聚合的根。
許多實體是聚合根,不包含其他實體。對于不可變的實體(相當于數據庫中的引用或靜態數據)尤其如此。示例可能包括Country,VehicleModel,TaxRate,Category,BookTitle等。
但是,更復雜的可變(事務)實體在建模為聚合時確實會受益,主要是通過減少概念開銷。我們不必考慮每個實體,而只考慮聚合根;聚合實體僅僅是聚合的“內部運作”。它們還簡化了實體之間的相互作用;我們遵循以下規則:(持久化)引用可能只是聚合的根,而不是聚合中的任何其他實體。
另一個DDD原則是聚合根負責確保聚合實體始終處于有效狀態。例如,Order(root)可能包含OrderItems的集合(聚合)。可能存在以下規則:訂單發貨后,任何OrderItem都無法更新。或者,如果兩個OrderItem引用相同的產品并具有相同的運輸要求,則它們將合并到同一個OrderItem中。或者,Order的派生totalPrice屬性應該是OrderItems的價格總和。維護這些不變量是root的責任。
但是......只有聚合根才能完全在聚合中維護對象之間的不變量。 OrderItem引用的產品幾乎肯定不會在AR中,因為還有其他用例需要與Product進行交互,而不管是否有訂單。因此,如果有一條規則不能對已停產的產品下達訂單,那么訂單將需要以某種方式處理。實際上,這通常意味著在訂單交易更新時使用隔離級別2或3來“鎖定”產品。或者,可以使用帶外過程來協調交叉聚合不變量的任何破壞。
在我們繼續前進之前退一步,我們可以看到我們有一系列粒度:
value < entity < aggregate < module < bounded context
現在讓我們繼續研究一些DDD構建塊。
存儲庫,工廠和服務(Repositories, Factories and Services)
在企業應用程序中,實體通常是持久的,其值表示這些實體的狀態。但是,我們如何從持久性存儲中獲取實體呢?
存儲庫是持久性存儲的抽象,返回實體 - 或者更確切地說是聚合根 - 滿足某些標準。例如,客戶存儲庫將返回Customer聚合根實體,訂單存儲庫將返回Orders(及其OrderItems)。通常,每個聚合根有一個存儲庫。
因為我們通常希望支持持久性存儲的多個實現,所以存儲庫通常由具有不同持久性存儲實現的不同實現的接口(例如,CustomerRepository)組成(例如,CustomerRepositoryHibernate或CustomerRepositoryInMemory)。由于此接口返回實體(域層的一部分),因此接口本身也是域層的一部分。接口的實現(與一些特定的持久性實現耦合)是基礎結構層的一部分。
我們搜索的標準通常隱含在名為的方法名稱中。因此,CustomerRepository可能會提供findByLastName(String)方法來返回具有指定姓氏的Customer實體。或者我們可以讓OrderRepository返回Orders,findByOrderNum(OrderNum)返回與OrderNum匹配的Order(請注意,這里使用值類型!)。
更復雜的設計將標準包裝到查詢或規范中,類似于findBy(Query <T>),其中Query包含描述標準的抽象語法樹。然后,不同的實現解包查詢以確定如何以他們自己的特定方式定位滿足條件的實體。
也就是說,如果你是.NET開發人員,那么值得一提的是LINQ [8]。因為LINQ本身是可插拔的,所以我們通常可以使用LINQ編寫存儲庫的單個實現。然后變化的不是存儲庫實現,而是我們配置LINQ以獲取其數據源的方式(例如,針對Entity Framework或針對內存中的對象庫)。
每個聚合根使用特定存儲庫接口的變體是使用通用存儲庫,例如Repository <Customer>。這提供了一組通用方法,例如每個實體的findById(int)。當使用Query <T>(例如Query <Customer>)對象指定條件時,這很有效。對于Java平臺,還有一些框架,例如Hades [9],允許混合和匹配方法(從通用實現開始,然后在需要時添加自定義接口)。
存儲庫不是從持久層引入對象的唯一方法。如果使用對象關系映射(ORM)工具(如Hibernate),我們可以在實體之間導航引用,允許我們透明地遍歷圖形。根據經驗,對其他實體的聚合根的引用應該是延遲加載的,而聚合中的聚合實體應該被急切加載。但與ORM一樣,期望進行一些調整,以便為最關鍵的用例獲得合適的性能特征。
在大多數設計中,存儲庫還用于保存新實例,以及更新或刪除現有實例。如果底層持久性技術支持它,那么它們很可能存在于通用存儲庫中,但是從方法簽名的角度來看,沒有什么可以區分保存新客戶和保存新訂單。
最后一點......直接創建新的聚合根很少見。相反,它們傾向于由其他聚合根創建。訂單就是一個很好的例子:它可能是通過客戶調用一個動作來創建的。
這整齊地帶給我們:
工廠
如果我們要求Order創建一個OrderItem,那么(因為畢竟OrderItem是其聚合的一部分),Order知道要實例化的具體OrderItem類是合理的。實際上,實體知道它需要實例化的同一模塊(命名空間或包)中的任何實體的具體類是合理的。
假設客戶使用Customer的placeOrder操作創建訂單(參見圖6)。如果客戶知道具體的訂單類,則意味著客戶模塊依賴于訂單模塊。如果訂單具有對客戶的反向引用,那么我們將在兩個模塊之間獲得循環依賴。
Figure 6: Customers and Orders (cyclic dependencie
如前所述,我們可以使用依賴性反轉原則來解決這類問題:從訂單中刪除依賴關系 - >客戶模塊我們將引入OrderOwner接口,使Order引用為OrderOwner,并使Customer實現OrderOwner(參見圖7))。
Figure 7: Customers and Orders (customer depends o
那么另一種方式呢:如果我們想要訂單 - >客戶? 在這種情況下,需要在客戶模塊中有一個表示Order的接口(這是Customer的placeOrder操作的返回類型)。 然后,訂單模塊將提供訂單的實現。 由于客戶不能依賴訂單,因此必須定義OrderFactory接口。 然后,訂單模塊依次提供OrderFactory的實現(參見圖8)。
可能還有相應的存儲庫接口。例如,如果客戶可能有數千個訂單,那么我們可能會刪除其訂單集合。相反,客戶將使用OrderRepository根據需要定位其訂單(的一部分)。或者(如某些人所愿),您可以通過將對存儲庫的調用移動到應用程序體系結構的更高層(例如域服務或應用程序服務)來避免從實體到存儲庫的顯式依賴性。
實際上,服務是我們需要探索的下一個話題。
域服務,基礎結構服務和應用程序服務(Domain services, Infrastructure services and Application services)
域服務(domain service)是在域層內定義的域服務,但實現可以是基礎結構層的一部分。存儲庫是域服務,其實現確實在基礎結構層中,而工廠也是域服務,其實現通常在域層內。特別是在適當的模塊中定義了存儲庫和工廠:CustomerRepository位于客戶模塊中,依此類推。
更一般地說,域服務是任何不容易在實體中生存的業務邏輯。埃文斯建議在兩個銀行賬戶之間進行轉賬服務,但我不確定這是最好的例子(我會將轉賬本身建模為一個實體)。但另一種域服務是一種充當其他有界上下文的代理。例如,我們可能希望與暴露開放主機服務的General Ledger系統集成。我們可以定義一個公開我們需要的功能的服務,以便我們的應用程序可以將條目發布到總帳。這些服務有時會定義自己的實體,這些實體可能會持久化;這些實體實際上影響了在另一個BC中遠程保存的顯著信息。
我們還可以獲得技術性更強的服務,例如發送電子郵件或SMS文本消息,或將Correspondence實體轉換為PDF,或使用條形碼標記生成的PDF。接口在域層中定義,但實現在基礎架構層中非常明確。因為這些非常技術性服務的接口通常是根據簡單的值類型(而不是實體)來定義的,所以我傾向于使用術語基礎結構服務(infrastructure service)而不是域服務。但是如果你想成為一個“電子郵件”BC或“SMS”BC的橋梁,你可以想到它們。
雖然域服務既可以調用域實體也可以調用域實體,但應用服務(application service)位于域層之上,因此域層內的實體不能調用,只能反過來調用。換句話說,應用層(我們的分層架構)可以被認為是一組(無狀態)應用服務。
如前所述,應用程序服務通常處理交叉和安全等交叉問題。他們還可以通過以下方式與表示層進行調解:解組入站請求;使用域服務(存儲庫或工廠)獲取對與之交互的聚合根的引用;在該聚合根上調用適當的操作;并將結果編組回表示層。
我還應該指出,在某些體系結構中,應用程序服務調用基礎結構服務。因此,應用服務可以直接調用PdfGenerationService,傳遞從實體中提取的信息,而不是實體調用PdfGenerationService將其自身轉換為PDF。這不是我的特別偏好,但它是一種常見的設計。我很快就會談到這一點。
好的,這完成了我們對主要DDD模式的概述。在Evans 500 +頁面書中還有更多內容 - 值得一讀 - 但我接下來要做的是突出顯示人們似乎很難應用DDD的一些領域。
問題和障礙
實施分層架構
這是第一件事:嚴格執行架構分層可能很困難。特別是,從域層到應用層的業務邏輯滲透可能特別隱蔽。
我已經在這里挑出了Java的EJB2作為罪魁禍首,但是模型 - 視圖 - 控制器模式的不良實現也可能導致這種情況發生。控制器(=應用層)會發生什么,承擔太多責任,讓模型(=域層)變得貧血。事實上,有更新的Web框架(在Java世界中,Wicket [10]是一個嶄露頭角的例子),出于這種原因明確地避免了MVC模式。
表示層模糊了域層
另一個問題是嘗試開發無處不在的語言。領域專家在屏幕方面談話是很自然的,因為畢竟,這就是他們可以看到的系統。要求他們在屏幕后面查看并在域概念方面表達他們的問題可能非常困難。
表示層本身也可能存在問題,因為自定義表示層可能無法準確反映(可能會扭曲)底層域概念,從而破壞我們無處不在的語言。即使不是這種情況,也只需要將用戶界面組合在一起所需的時間。使用敏捷術語,速度降低意味著每次迭代的進度較少,因此對整個域的深入了解較少。
存儲庫模式的實現
從更技術性的角度來看,新手有時似乎也會混淆將存儲庫(在域層中)與其實現(在基礎架構層中)的接口分離出來。我不確定為什么會這樣:畢竟,這是一個非常簡單的OO模式。我想這可能是因為埃文斯的書并沒有達到這個細節水平,這讓一些人變得高高在上。但這也可能是因為替換持久性實現(根據六邊形體系結構)的想法并不普遍,導致持久性實現滲透到域層的系統。
服務依賴項的實現
另一個技術問題 - 在DDD從業者之間可能存在分歧 - 就實體與域/基礎設施服務(包括存儲庫和工廠)之間的關系而言。有些人認為實體根本不應該依賴域服務,但如果是這種情況,則外部應用程序服務與域服務交互并將結果傳遞給域實體。根據我的思維方式,這使我們走向了一個貧血的領域模型。
稍微柔和的觀點是實體可以依賴于域服務,但應用程序服務應該根據需要傳遞它們,例如作為操作的參數。我也不喜歡這個:對我而言,它將實現細節暴露給應用層(“這個實體需要這樣一個服務才能完成這個操作”)。但是許多從業者對這種方法感到滿意。
我自己的首選方案是使用依賴注入將服務注入實體。實體可以聲明它們的依賴關系,然后基礎結構層(例如Hibernate,Spring或其他一些框架)可以將服務注入實體:
public class Customer {
…
private OrderFactory orderFactory;
public void setOrderFactory(OrderFactory orderFactory) {
this.orderFactory=orderFactory;
}
…
public Order placeOrder( … ) {
Order order=orderFactory.createOrder();
…
return order;
}
}
一種替代方法是使用服務定位器模式。例如,將所有服務注冊到JNDI中,然后每個域對象查找它所需的服務。在我看來,這引入了對運行時環境的依賴。但是,與依賴注入相比,它對實體的內存需求較低,這可能是一個決定性因素。
不合適的模塊化
正如我們已經確定的那樣,DDD在實體之上區分了幾種不同的粒度級別,即聚合,模塊和BC。獲得正確的模塊化水平需要一些練習。正如RDBMS模式可能被非規范化一樣,系統也沒有模塊化(成為泥漿的大球)。但是,過度規范化的RDBMS模式(其中單個實體在多個表上被分解)也可能是有害的,過模塊化系統也是如此,因為它變得難以理解系統如何作為整體工作。
我們首先考慮模塊和BC。記住,模塊類似于Java包或.NET命名空間。我們希望兩個模塊之間的依賴關系是非循環的,但是如果我們確定(比如說)客戶依賴于訂單,那么我們不需要做任何額外的事情:客戶可以簡單地導入Order包/命名空間并使用它接口和類根據需要。
但是,如果我們將客戶和訂單放入單獨的BC中,那么我們還有更多的工作要做,因為我們必須將客戶BC中的概念映射到BC訂單的概念。在實踐中,這還意味著在客戶BC中具有訂單實體的表示(根據前面給出的總分類帳示例),以及通過消息總線或其他東西實際協作的機制。請記住:擁有兩個BC的原因是當有不同的最終用戶和/或利益相關者時,我們無法保證不同BC中的相關概念將朝著相同的方向發展。
另一個可能存在混淆的領域是將實體與聚合區分開來。每個聚合都有一個實體作為其聚合根,對于很多很多實體,聚合將只包含這個實體(“瑣碎”的情況,正如數學家所說的那樣)。但我看到開發人員認為整個世界必須存在于一個聚合中。因此,例如,訂單包含引用產品的OrderItems(到目前為止一直很好),因此開發人員得出結論,產品也在聚合中(不!)更糟糕的是,開發人員會觀察到客戶有訂單,所以想想這個意味著我們必須擁有Customer / Order / OrderItem / Product的巨型聚合(不,不,不!)。關鍵是“客戶有訂單”并不意味著暗示匯總;客戶,訂單和產品都是集合的根源。
實際上,一個典型的模塊(這是非常粗糙和準備好的)可能包含六個聚合,每個聚合可能包含一個實體和幾個實體之間。在這六個中,一個好的數字可能是不可變的“參考數據”類。還要記住,我們模塊化的原因是我們可以理解一件事(在一定的粒度級別)。所以要記住,典型的人一次只能保持在5到9個之間[11]。
入門
正如我在開始時所說,你可能在DDD之前遇到過很多想法。事實上,我所說過的每一個Smalltalker(我不是一個,我不敢說)似乎很高興能夠在EJB2等人的荒野歲月之后回歸域驅動的方法。
另一方面,如果這些東西是新的怎么辦?有這么多不同的方式來絆倒,有沒有辦法可靠地開始使用DDD?
如果你環顧一下Java領域(對.NET來說并不那么糟糕),實際上有數百個用于構建Web應用程序的框架(JSP,Struts,JSF,Spring MVC,Seam,Wicket,Tapestry等)。從持久性角度(JDO,JPA,Hibernate,iBatis,TopLink,JCloud等)或其他問題(RestEasy,Camel,ServiceMix,Mule等),有很多針對基礎架構層的框架。但是很少有框架或工具來幫助DDD所說的最重要的層,即域層。
自2002年以來,我一直參與(現在是一個提交者)一個名為Naked Objects的項目,Java上的開源[12]和.NET上的商業[13]。雖然Naked Objects沒有明確地開始考慮領域驅動的設計 - 事實上它早于Evans的書 - 它與DDD的原理非常相似。它還可以輕松克服前面提到的障礙。
您可以將Naked Objects視為與Hibernate等ORM類似。 ORM構建域對象的元模型并使用它來自動將域對象持久保存到RDBMS,而Naked Objects構建元模型并使用它在面向對象的用戶界面中自動呈現這些域對象。
開箱即用的Naked Objects支持兩個用戶界面,一個富客戶端查看器(參見圖9)和一個HTML查看器(參見圖10)。這些都是功能完備的應用程序,需要開發人員只編寫要運行的域層(實體,值,存儲庫,工廠,服務)。
Figure 9: Naked Objects Drag-n-Drop Viewer
我們來看看Claim類的(Java)代碼(如屏幕截圖所示)。首先,這些類基本上是pojos,盡管我們通常從便捷類AbstractDomainObject繼承,只是為了分解注入通用存儲庫并提供一些幫助方法:
public class Claim extends AbstractDomainObject {
...
}
Next, we have some value properties:
// {{ Description
private String description;
@MemberOrder(sequence="1")
public String getDescription() { return description; }
public void setDescription(String d) { description=d; }
// }}
// {{ Dateprivate Date date;@MemberOrder(sequence="2")public Date getDate() { return date; }public void setDate(Date d) { date=d; }// }}
// {{ Statusprivate String status;@Disabled@MemberOrder(sequence="3")public String getStatus() { return status; }public void setStatus(String s) { status=s; }// }}
這些是簡單的getter / setter,返回類型為String,日期,整數等(盡管Naked Objects也支持自定義值類型)。接下來,我們有一些參考屬性:
// {{ Claimant
private Claimant claimant;
@Disabled
@MemberOrder(sequence="4")
public Claimant getClaimant() { return claimant; }
public void setClaimant(Claimant c) { claimant=c; }
// }}
// {{ Approverprivate Approver approver;@Disabled@MemberOrder(sequence="5")public Approver getApprover() { return approver; }public void setApprover(Approver a) { approver=a; }// }}
這里我們的Claim實體引用其他實體。實際上,Claimant和Approver是接口,因此這允許我們將域模型分解為模塊,如前所述。
實體也可以擁有實體集合。在我們的案例中,Claim有一個ClaimItems的集合:
// {{ Items
private List<ClaimItem> items=new
ArrayList<ClaimItem>();
@MemberOrder(sequence="6")
public List<ClaimItem> getItems() { return items; }
public void addToItems(ClaimItem item) {
items.add(item);
}
// }}
我們還有(Naked Objects調用的)動作,即submit和addItem:這些都是不代表屬性和集合的公共方法:
// {{ action: addItem
public void addItem(
@Named("Days since")
int days,
@Named("Amount")
double amount,
@Named("Description")
String description) {
ClaimItem claimItem=newTransientInstance(ClaimItem.class);
Date date=new Date();
date=date.add(0,0, days);
claimItem.setDateIncurred(date);
claimItem.setDescription(description);
claimItem.setAmount(new Money(amount, "USD"));
persist(claimItem);
addToItems(claimItem);
}
public String disableAddItem() {
return "Submitted".equals(getStatus()) ? "Already
submitted" : null;
}
// }}
// {{ action: Submit
public void submit(Approver approver) {
setStatus("Submitted");
setApprover(approver);
}
public String disableSubmit() {
return getStatus().equals("New")?
null : "Claim has already been submitted";
}
public Object[] defaultSubmit() {
return new Object[] { getClaimant().getApprover() };
}
// }}
這些操作會在Naked Objects查看器中自動呈現為菜單項或鏈接。而這些行動的存在意味著Naked Objects應用程序不僅僅是CRUD風格的應用程序。
最后,有一些支持方法可以顯示標簽(或標題)并掛鉤持久性生命周期:
// {{ Title
public String title() {
return getStatus() + " - " + getDate();
}
// }}
// {{ Lifecyclepublic void created() {status="New";date=new Date();}// }}
之前我將Naked Objects域對象描述為pojos,但您會注意到我們使用注釋(例如@Disabled)以及命令式幫助器方法(例如disableSubmit())來強制執行業務約束。 Naked Objects查看器通過查詢啟動時構建的元模型來尊重這些語義。如果您不喜歡這些編程約定,則可以更改它們。
典型的Naked Objects應用程序由一組域類組成,例如上面的Claim類,以及存儲庫,工廠和域/基礎結構服務的接口和實現。特別是,沒有表示層或應用層代碼。那么Naked Objects如何幫助解決我們已經確定的一些障礙?
實施分層架構:因為我們編寫的唯一代碼是域對象,域邏輯無法滲透到其他層。實際上,Naked Objects最初的動機之一就是幫助開發行為完整的對象
表示層模糊了域層:因為表示層是域對象的直接反映,整個團隊可以迅速加深對域模型的理解。默認情況下,Naked Objects直接從代碼中獲取類名和方法名,因此強烈要求在無處不在的語言中獲得命名權。通過這種方式,Naked Objects也支持DDD的模型驅動設計原理
存儲庫模式的實現:您可以在屏幕截圖中看到的圖標/鏈接實際上是存儲庫:EmployeeRepository和ClaimRepository。 Naked Objects支持可插入對象存儲,通常在原型設計中,我們使用針對內存中對象存儲的實現。當我們轉向生產時,我們會編寫一個實現數據庫的實現。
服務依賴項的實現:Naked Objects會自動將服務依賴項注入每個域對象。這是在從對象庫中檢索對象時,或者首次創建對象時完成的(請參閱上面的newTransientInstance())。事實上,這些輔助方法所做的就是委托Naked Objects提供的名為DomainObjectContainer的通用存儲庫/工廠。
不合適的模塊化:我們可以通過正常方式使用Java包(或.NET命名空間)模塊化為模塊,并使用Structure101 [14]和NDepend [15]等可視化工具來確保我們的代碼庫中沒有循環依賴。我們可以通過注釋@Hidden來模塊化為聚合,任何聚合對象代表我們可見聚合根的內部工作;這些將不會出現在Naked Objects查看器中。我們可以編寫域和基礎設施服務,以便根據需要橋接到其他BC。
Naked Objects提供了許多其他功能:它具有可擴展的體系結構 - 特別是 - 允許實現其他查看器和對象存儲。正在開發的下一代觀眾(例如Scimpi [16])提供更復雜的定制功能。此外,它還提供多種部署選項:例如,您可以使用Naked Objects進行原型設計,然后在進行生產時開發自己的定制表示層。它還與FitNesse [17]等工具集成,可以自動為域對象提供RESTful接口[18]。
下一步
領域驅動的設計匯集了一組用于開發復雜企業應用程序的最佳實踐模式。一些開發人員多年來一直在應用這些模式,對于這些人來說,DDD可能只是對他們現有實踐的肯定。但對于其他人來說,應用這些模式可能是一個真正的挑戰。
Naked Objects為Java和.NET提供了一個框架,通過處理其他層,團隊可以專注于重要的部分,即域模型。通過直接在UI中公開域對象,Naked Objects允許團隊非常自然地構建一個明確無處不在的語言。隨著域層的建立,團隊可以根據需要開發更加量身定制的表示層。
lace an order
“落盤”
*請認真填寫需求信息,我們會在24小時內與您取得聯系。