在本文中,我將介紹一種實現富域模型的可能方法-此示例使用Guice ,盡管我確信Spring等將具有實現同一目標的不同方法。
問題
所有源代碼都可以在github上找到。 “ master”分支顯示原始的,分解不良的代碼。 “富域”分支顯示了我描述的解決方案。
貧血領域模型
首先,我們的貧血領域模型– TradeOrder.java 。 像Hibernate一樣,該類具有大量的注釋,這些注釋描述了數據模型,所有數據的字段,訪問數據的訪問器和變異器,并且沒有其他有趣的東西。 我假設在此領域中,TradeOrders可以完成任務。 也許我們下訂單或取消訂單。 沿線某個地方,我們域中的關鍵對象可能應該具有某些行為。
@Entity @Table(name="TRADE_ORDER") public class TradeOrder {@Id@Column(name="ID", length=32)@GeneratedValueprivate String id;@ManyToOne@JoinColumn(name="CURRENCY_ID", nullable=false)@ForeignKey(name="FK_ORDER_CURRENCY")@AccessType("field")private Currency currency;@Column(name="AMOUNT", nullable=true)private BigDecimal amount;public TradeOrder() { }public String getId() { return id; }public Currency getCurrency() { return currency; }public void setCurrency(Currency currency) { this.currency = currency; }public BigDecimal getAmount() { return amount; }public void setAmount(BigDecimal amount) { this.amount = amount; } }
助手類
在這個非常簡單的示例中,我們需要計算訂單的價值,即我們要購買(或出售)的股票數量和所支付的每股價格。 不幸的是,因為這涉及到依賴關系,所以該行為并不駐留在與其相關的類中,而是被轉移到了一個非常有用的幫助程序類中。
看一下FiguresFactory.java 。 該類只有一個公共方法– buildFrom。 此方法的目標是從TradeOrder創建圖形。
public Figures buildFrom(TradeOrder order, Date effectiveDate)throws OrderProcessingException {Date tradeDate = order.getTradeDate();HedgeFundAsset asset = order.getAsset();BigDecimal bestPrice = bestPriceFor(asset, tradeDate);return order.getType() == TradeOrderType.REDEMPTION? figuresFromPosition(order,lookupPosition(asset, order.getFohf(), tradeDate),lookupPosition(asset, order.getFohf(), effectiveDate),bestPrice): getFigures(order, bestPrice, null); }
除了“生效日期”(無論可能是什么)之外,此方法僅需要輸入TradeOrder。 使用TradeOrder上大量的吸氣劑,它會要求操作數據,而不是告訴 TradeOrder需要什么。 在理想的,面向對象的系統中,這應該是TradeOrder上稱為createFigures的方法。
我們為什么到這里來? 都是依賴注入框架的錯! 因為創建Figures對象的過程需要我們解析價格和貨幣,所以我們需要使用可注入的依賴關系去查找這些數據。 我們的貧血領域無法注入依賴項,因此我們將其注入到這個小助手類中。
我們最終得到的是經典的貧血域模型。 TradeOrder具有數據; 而許多輔助類(例如FiguresFactory)包含對數據進行操作的行為。 都是非OO的。
更好的方法
資料記錄
第一步是創建一個簡單的值對象以映射數據庫中的行–我將其稱為TradeOrderRecord.java 。 這看起來很像原始域對象,只是我刪除了訪問器和增變器以清楚地表明這是一個沒有行為的值對象。
為了使構造這些記錄對象更容易,我使用了karg,這是我的一位同事編寫的一個庫–這要求我們聲明可用于構造記錄的一組參數,以及一個接受參數列表的構造函數。 這極大地簡化了構造,并且避免了我們擁有需要27個字符串的構造函數(例如)。
@Entity @Table(name="TRADE_ORDER") public class TradeOrderRecord {@Id@Column(name="ID", length=32)@GeneratedValuepublic String id;@Column(name="CURRENCY_ID")public String currencyId;@Column(name="AMOUNT", nullable=true)public BigDecimal amount;public static class Arguments {public static final Keyword<String> CURRENCY_ID = newKeyword();public static final Keyword<BigDecimal> AMOUNT = newKeyword();}protected TradeOrderRecord() { }public TradeOrderRecord(KeywordArguments arguments) {this.currencyId = Arguments.CURRENCY_ID.from(arguments);this.amount = Arguments.AMOUNT.from(arguments);} }
富域
我們的目標是使TradeOrder成為一個豐富的域對象-它應該具有與“ TradeOrder”的域概念相關的所有行為和數據。
數據
TradeOrder首先需要在內部存儲與TradeOrder相關的所有數據(至少作為起點,未使用的字段暗示我們可能能夠進一步簡化此操作)。
public class TradeOrder {private final String id;private final String currencyId;private final BigDecimal amount;
我們使數據不可變 。 不可變狀態通常是一件好事–在這里,它迫使我們清楚這是一個完全填充的TradeOrder,并且由于它具有ID,因此它始終與數據庫中的一行關聯。 通過使TradeOrder不可變,顯而易見的問題是–如何更新訂單? 嗯,我們可以選擇多種方式來做到這一點–但這是在不同時間的不同故事。
我們也不需要訪問器。 由于我們計劃將與TradeOrder相關的所有行為放在TradeOrder類本身上,因此其他類不需要詢問信息,它們只需要告訴我們他們想要實現什么。
注意:有一個(現在不建議使用)訪問器–暗示應進一步移動的行為。
依存關系
除了用于存儲數據的字段之外,TradeOrder還將具有表示可注入依賴項的字段。
private final CurrencyCache currencyCache; private final PriceFetcher bestPriceFetcher; private final PositionFetcher hedgeFundAssetPositionsFetcher; private final FXService fxService;
有些人會發現這種冒犯性-將依賴項與數據混合在一起。 但是,就我個人而言,我認為這種權衡是值得的–能夠在與之相關的類上定義我們的行為的好處是值得的。
行為
現在我們將數據和依賴項全部集中在一處,在FiguresFactory的方法之間移動相對容易:
public Figures createFigures(Date effectiveDate) throws OrderProcessingException {BigDecimal bestPrice = bestPriceFor(this.asset, this.tradeDate);return this.type == TradeOrderType.REDEMPTION? figuresFromPosition(fohf,lookupPosition(this.asset, fohf, this.tradeDate),lookupPosition(this.asset, fohf, effectiveDate), bestPrice): getFigures(fohf, bestPrice, null); }
施工
我們需要解決的最后一件事是如何創建TradeOrder實例。 由于數據和依賴項的字段都標記為final,因此構造函數必須將它們全部初始化。 這意味著我們需要一個帶有依賴關系的構造函數和一個TradeOrderRecord(即我們從數據庫中讀取的值對象):
@Inject protected TradeOrder(CurrencyCache currencyCache,PriceFetcher bestPriceFetcher,PositionFetcher hedgeFundAssetPositionsFetcher,FXService fxService,@Assisted TradeOrderRecord record) {... }
這不是特別漂亮,但是要注意的關鍵是@Assisted注釋。 這使我們可以告訴Guice,其他依賴項已正常注入,而TradeOrderRecord應該從工廠方法傳遞過來。 工廠界面本身如下所示:
public static interface Factory {TradeOrder create(TradeOrderRecord record); }
我們不需要實現此接口,Guice會自動提供它。 當需要創建TradeOrder實例時,可以在其他地方使用TradeOrder.Factory成為可注入依賴項。 Guice會像平常一樣初始化可注入依賴關系,輔助依賴關系– TradeOrderRecord –從工廠傳遞過來。 因此,我們的調用代碼無需擔心我們的富域需要可注入的依賴項。
@Inject private TradeOrder.Factory tradeOrderFactory; ... TradeOrderRecord record = tradeOrderDAO.loadById(id); TradeOrder order = tradeOrderFactory.create(record);
結論
通過將依賴項和數據混合到一個豐富的域模型中,我們可以定義具有正確行為的類。 現在,TradeOrder中明顯的代碼味道是創建圖形的詳細機制可能是一個單獨的問題,應予以分解。 沒關系,我們可以引入一個新的依賴項來進行管理-只要TradeOrder仍然是構造Figures對象的起點。
通過將行為放在一個地方,我們的域模型更易于使用,更易于推理,也更容易發現重復或相似的情況。 然后,我們可以使用良好的對象模型進行合理的重構,而不是將任意的區別引入到作為函數庫而不是域參與者的幫助器類中。
參考: 實施富域M 來自JCG合作伙伴 David Green的Guice先生在Actively Lazy Blog上發表的評論 。
- 在領域驅動的設計,貧乏的領域模型,代碼生成,依賴項注入等方面……
- 在域驅動設計中使用狀態模式
- Spring和AspectJ的領域驅動設計
- Spring依賴注入技術的發展
- 什么是依賴倒置? 是IoC嗎?
翻譯自: https://www.javacodegeeks.com/2011/10/rich-domain-model-with-guice.html