在本文中,我将介绍一种实现富域模型的可能方法-此示例使用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