戏说领域驱动设计(廿三)——工厂
在讲解实体的章节中我们曾经介绍说过如何有效的创建实体,主要包括两种方式:工厂和构造函数。本章我们工厂进行一下详解,这种东西能有效的简化实体使用的难度,毕竟你无法通过Spring这种容器来管理领域对象。实际的开发过程中,工厂的使用要比书中的讲解会复杂一点,所以在本章我会对实践中遇到的一些问题以及使用什么样的模式去应对给出一些建议。
一、工厂的作用
学习过设计模式的人都应该知道“工厂模式”,尤其是其中的“简单工厂”,感觉就没什么可学的,太简单了。但在DDD中,工厂却比较常用,不过也正像书上说的一样,其实算不上一等公民,毕竟其承担的责任只是实体的创建,有点偏技术。但反过来说,少了这么一个东西还真不行,有些实体的创建起来很费劲,大部分情况下只有实体设计人能完全搞定,出现了知识垄断的情况。可是在真实的工作中,我们需要团队协作,也会出现人员更迭的情况,出现这种垄断并不是什么好事儿。此外,作为设计者,让自己研发出来的东西特别难以使用,这本身其实是失败的。看看Spring框架,你就知道人家工程师的牛掰之处了,咱不管其内部如何复杂,你就告诉我使用起来是不是很方便吧?我这里有个小经验与大家分享:不论是做后台的代码还是前端的功能,都把自己假设成为用户,你就会在设计过程中自然而然的考虑易用性和安全性了。当然,也不排除有些不愿意思考的人,不过是自废前程而矣。将自己当成用户还有另外一个好处:之所以叫用户,就代表你不能对他做任何假设,只要你提供出去功能就代表是可用的,把自己当成用户正好可以检验代码中是否存在不妥之处。之前我们说过实体的不变条件,当把客户作为不可信任对象看待的时候,你就会在设计过程中增加约束来避免破坏不变性的情况出现。
扯扯就远了,看看下面这段代码,这是我在实际的项目中所设计的一个实体。前面我曾经说过,实体中必须包含一个可以让所有属性得到有效赋值的构造函数,因为保障它的完整性和不变条件是在实体设计过程中需要遵守的重要原则。
public class DeploymentApprovalForm extends ApprovalFormBase { DeploymentApprovalForm(Long id, String name, ApplierInfo applierInfo, LocalDateTime createdDate, LocalDateTime updatedDate, List<ApprovalNodeBase> nodes, LocalDateTime deploymentDate, ProcessStatus status, PhaseType currentPhase, String service, ApplyType applyType) { super(id, name, applierInfo, createdDate, updatedDate, nodes); if (status != null) { this.status = status; } this.deploymentDate = deploymentDate; if (currentPhase != null && currentPhase != PhaseType.UNKNOWN) { this.currentPhase = currentPhase; } this.changeService(service); this.applyType = applyType; if (applyType == null || applyType == ApplyType.UNKNOWN) { this.applyType = ApplyType.FORMAL; } } }
我如果直接把这样的设计给其它程序员使用,保准被骂爹!这个对象的构造太复杂了,你需要了解每一个参数是如何构造了。简单类型还好,其中还包含了许多的值对象,使用人需要了解每一个值对象的构造方式和理,别跟我说使用Spring 的IoC,这可是领域对象。其实也不是故意要写成这样,业务复杂的情况实体也不可能简单了,要不然谁还用OOP,整个面向过程不是挺香的吗?您其实不需要考虑上述代码是什么含义,只需要关注其构造函数即可。之所以给出这段代码,是想向您证明我们本章的主题:虽然工厂不是一等公平,但不代表其不重要。当然了,你可能会抬杠说没有工厂就不能创建对象了?也不是不行,成本高啊。如果这段代码是别人写的,现在你要用,我就问你是不是得问对方怎么搞,没人可问的话你是不是需要自己把代码都看一遍?一个实体这样干可以,十个呢?百个呢?这不是工作,是自虐!针对上述代码,您可能还会说可以使用视图模型作为参数,相当于把构造函数作为工厂来使用。这种情况下的确可以隐藏对象创建细节,不过领域模型主要是用于为某个业务的执行进行支撑,过重的构造函数从另一方面又增加了其责任。另外就是代码量很大,反正我觉得这样做不好,单一责任原则其实是值得遵守的。
回归正题,对于上面的反例,相信在此刻我根本不需要再解释引入工厂的好处,事实已经证明了。这样的场景我相信您在实践中肯定遇到过,而且不会少,那么要如何使用工厂,请继续跟着我的脚步前行。
二、工厂使用模式
工厂模式的使用有三种,您可别一见到工厂就以为需要创建一个“*Factory”的类,这种方式的确比较常用,但并不是全部。不同的场景需要使用不同的方法,毕竟我们考虑问题的时候不能太过于狭隘,实现情况还是很复杂的。
1、实体包含工厂方法
一种经常被使用的方式是在实体中加入用于创建该实体的静态方法,如下面代码片段所示。在实体不那么复杂的情况下,这种方式其实可以接受,虽然说这样会造成实体承担了过多的责任,不过在实践中有些模棱两可的规则是可以打破。您完全可以新建一个单独的类,责任虽然单一了,可又多了一个类文件,维护起来也是需要成本的。
public class Order extends EntityModel<Long> { private String name; public static Order create(OrderVo orderInfo) { …… } }
另外一种方式是通过实体中的业务方法创建另外的实体,这种方法最常见于领域事件的创建,如下代码片段所示。此种方式所带来的好处是其有效的表达出了所谓的通用语言,直白来说就是反应了业务术语。我早期写代码的时候谨遵一个模式:命令型方法无返回值,我记得应该是在《代码大全》中有过类似的说明。所以遇到需要使用事件的场景,都是在应用服务中进行构造。近两年则使用类似下面这种方式,这代码看起来多么优雅,所以各位看君切莫像我一样陷入教条主义。
public class Order extends EntityModel<Long> { private OrderStatus staus; public OrderPaid pay(Money fee) { this.status = OrderStatus.PAID; return new OrderPaid(this.getId()); } }
什么?你怀疑我水文字,上述的案例看不出来哪里反应了通用语言?较劲呗?那我就再整一个。我曾经设计过一个类似工作流的东西,叫作“业务申请单”,你也不管到底申请什么的,反正有申请就会涉及到审批,需求中说明“每次审批的操作都需要记录操作结果,用户可以查看某个审批单的所有操作记录”。下面为部分代码的片段,通过示例您可以看到“ApprovalFormBase”实体的“approve”方法在业务执行完结后返回一个“审批记录”实体,这里它不仅承担了工厂的作用,也表达了业务意图。说到这份儿应该不能算是水文字了吧?
public abstract class ApprovalFormBase extends EntityModel<Long> { private ApprovalNodeGroup nodeGroup = new ApprovalNodeGroup(); public ApprovalRecord approve(Advice advice) throws ApprovalFormOperationException { this.throwExceptionIfTerminatedOrInvalidated(); if (advice == null) { throw new ApprovalFormOperationException(OperationMessages.INVALID_APPROVAL_INFO); } …… return this.nodeGroup.approve(approvalContext, advice); } }
2、实体的子类作为工厂
这种方式在本系列的第十六章中介绍过,相对来说也比较优雅,虽然多出来一个新的文件。方便起见,我还是把代码再贴一下并稍微多做一些解释。“Order”代码中,我将其构造函数设计为“protected”,这样就可以限制住不经过工厂而创建其实例的情况。另外,这种方式也可以让您在工厂类中调用一些父类的方法,实践中此等应用场景并不多见,因为工厂的职责只能用于实体的实例化不应承担业务规则,不过也让我们在开发工作中遇到某些需要抉择的场景时多了一个选择。
public class Order extends EntityModel<Long> { private String name; private Contact contact; protected Order(Long id, String name, Contact contact) throws OrderCreationException { super(id); this.name = name; this.contact = contact; } } final public class OrderFactory extends Order { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } Contact contact = new Contact(orderInfo.getEmail(), orderInfo.getName()); return new Order(0L, orderInfo.getName(), contact); } }
3、业务服务类作为工厂
业务服务类作为工厂其实类似于上面的工厂子类,只是这种工厂并不会从某个实体继承。这种方式其实在实践中比较常用,因为够直观。虽然我们通常会采用“*Factory”这样的命名方式,但其本质上是一个领域服务(回想一下领域服务的使用规则)。通常情况下,我们工厂服务存在两个使用模式:一是简单领域实体工厂,此种模式使用方式简单明了,一目了然,请参看如下代码。此处请您务必注意一下,下面的代码片段仅仅是为演示用,真实的场景下代码相对要复杂一点,本章后面部分我会着重以此说明;工厂服务另外的一个模式使用起来简单,不过其具备较强的业务含义,下一节我会对此做详细解释。不过在继续之前,我们给下面这种工厂一个名字以方便后面引用,就叫其为“实体工厂”吧。
final public class OrderFactory{ public final static OrderFactory INSTANCE = new OrderFactory(); private OrderFactory() { } public Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } Contact contact = new Contact(orderInfo.getEmail(), orderInfo.getName()); return new Order(0L, orderInfo.getName(), contact); } }
工厂服务的第二个模式在命名上一般不会使用“*Factory”模式,而是使用“*Service”代替之,其包含的创建型方法基本上只用于构造新的对象;而“实体工厂”除了此项责任外还会用于实体数据反序列化后的构造。为方便起见,我们给第二个模式所描述的工厂一个新的名称“工厂服务”,下面我们来着重介绍一下“工厂服务”的使用。
举一个例子更能说明问题,这个业务很简单:订单项需要包含要购买的商品信息。通过名字您可以看出来“订单项”与“商品”肯定属于两个不同的限界上下文:一个是订单BC,一个是销售品BC。两个限界上下文间只能通过什么对象来传递信息来着?“视图模型”,千万别忘了。订单项是一个领域模型,从销售品限界上下文传过来的信息是一个视图模型,这两个对象不能放在一起,这个应该不会有疑问吧?此外,销售品域中的销售品信息属性非常多比如“规格”、“生产厂商”、“质量保证信息”等,但传到订单域后也就一两种是被使用的。您也见天儿在淘宝或京东买东西,没见订单项中包含生产厂家、详细规格等信息吧?这些根本就不是订单项所关注的内容,它所在意的是:产品名称、价格。假如我们在深入想一想,你所买的东西在销售品域中其实不能被称之为“商品”的,它还没被销售出去,叫商品不合适;而到了订单域后,它已经被订购了,此刻才能真正的被称之为商品。当然了,“商品”也好、“销售品”也好,叫什么听领域专家的,这是人为的规定,案例中的叫法也只是为了演示效果。其实类似的例子我在前面已经举过,即“订单和客户信息的领域模型设计”。之所以再拿出来说明,是想让您在设计过程中要注意通用语言的使用以及从始至终都通过业务来驱动领域模型设计的工作思路。其实通用语言这个概念挺虚的,您只需要遵守如下原则:在设计过程中仔细考虑领域模型的命名,这个命名一旦在沟通中使用,大家就会明白其具体指向的是什么;通过阅读代码也能知晓某个实体所指代的领域对象。对于上面的需求,我们的代码可以写成下面这样。
final public class GoodsCreatorService { public final static GoodsCreator INSTANCE = new GoodsCreator(); private GoodsCreator() { } public List<Goods> create(List<ProductVO) products) { return products.stream() .map(e -> new Goods(e.getName(), e.getID())) .collect(Collectors.toList()); } }
在上面的代码中,“create”方法的参数“products”由应用服务调用销售品BC适配器获取并传入到“GoodsCreatorService”中,请务必别忘了这是一个领域服务,不要让其直接调用基础设施层的适配器。
三、实体工厂实践
我特意把“实体工厂”的设计提取出来,是因为在实践中需要关注工厂的构建方法所适用的场景,并不是只有一个如“create”或“build”方法就能搞定的。前面我们说过,实体的创建有两个场景:一是根据外部信息从无到有的创建;二是根据数据库信息反序列化。虽然本质上都是进行实体的创建,但由于场景不同,其实现思路也不一样,让我们仔细的说。
新建实体时我们有时会根据业务需要硬性的给某个实体属性一个默认值;构建过程中如果外部信息不全,我们也可能需要给其某个属性一个默认值,比如下面的代码片段。这段代码展示了:1)新建订单时将其状态强制设置为“待支付”;2)“是否需要发票”属性如未在参数中包含信息则默认为“否”。这段代码看起来没有错误,但不能用于实体反序列化时,否则每次从数据库反序列化后订单的状态都是“待支付”。实体序列化后必然会涉及反序列化的过程,除非你只序列一次,那不就成了日志了吗?
final public class OrderFactory { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } status = OrderStatus.WAIT_PAY; boolean needFapiao = false; if (orderInfo.needFapiao() != null) { needFapiao = true; } return new Order(0L, status, needFapiao); } } public enum OrderStatus { public static OrderStatus of(Integer status) { if (status == null) { return OrderStatus.UNKNOWN } } }
我其实等着您回怼呢,你可能会说“你这代码是骗人的,我可以首先判断传入的状态信息是否为空,为空时我再设置默认值;不为空我就使用传入的值”,也就是下面这段代码。其实这段代码才会有潜在的问题:如果某个工程师手欠,把数据库中订单“状态”列的值变成了“null”,这种订单从数据库反序列化后会出现什么结果?实际上从数据的层面来看已经违反了业务的约束,这种对象在创建过程中应该报错。但如果按下面代码的方式,往小了看是一个Bug,往大了看可能会引发更多的账务问题或投诉。实践中,如果对象属性多、创建复杂时,创建过程可能会引发比较大的问题。看得到的还能及时处理,那些潜在的问题才是致命的。此等情况下简单的使用上面的实体工厂肯定不行,亲爱的屏幕前的您,何解?
final public class OrderFactory { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } OrderStatus status = OrderStatus.of(orderInfo.getStatus()); if (status == OrderStatus.UNKNOWN) { status = OrderStatus.WAIT_PAY; } return new Order(0L, status, needFapiao); } } public enum OrderStatus { public static OrderStatus of(Integer status) { if (status == null) { return OrderStatus.UNKNOWN } } }
在说出答案前我其实挺想展示一下在实际项目中工厂方法的复杂度的真实情况,不过贴出这些案例反而会影响我们叙述的思路。所以我先针对上述的问题给出解决方案:既然创建对象会出现在两个场景中即新建和加载,而我们期望实体的创建不论针对哪种场景最好都通过一个工厂来完成。那我们就索性为每个场景都创建一个单独的方法并统一放到一个工厂对象中,如下代码所示。这是一个实体工厂的基类,我们定义了两个用于实体创建的方法。当然,您也可以根据需要决策是否建立这样的基类,因为我们更强调思想的正确。
public abstract class EntityFactoryBase<TEntity extends EntityModel, TParameter extends VOBase> { protected abstract TEntity create(TParameter modelInfo) throws OrderCreationException; protected abstract TEntity load(TParameter modelInfo) throws OrderCreationException; }
别震惊啊,就这么简单,这里唯一的约束是:你在创建或从持久化设施加载领域实体的时候,参数应该是“视图模型”。因为工厂主要就是为了应对复杂场景而存在的,你构造一个对象就三个参数,要毛线的工厂啊。方法的实现我不给代码了,“create”和前面的示例一样,可做一些初始化或默认值的工作;“load”方法,根据传入的参数(这些参数来源于持久化设施,查询出来后将数据模型转换为视图模型),不做任何的默认值设定。要不还是写一下“load”吧,免得您说我只打嘴炮儿。
final public class OrderFactory extends EntityFactoryBase<Order, OrderVO> { public final static OrderFactory INSTANCE = new OrderFactory(); public Order load(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } //代码省略 return new Order(0L, orderInfo.getStatus()); } } public class OrderRepository { private OrderMapper orderMapper; public Order findBy(Long id) { OrderDataEntity entity = this.orderMapper.getById(id); OrderVO orderInfo = OrderVO.of(entity); return OrderFactory.INSTANCE.load(orderInfo); } }
上述的解决方案其实很简单,您在使用的时候完全可以使用不同的方式。我之所以特意提出是因为在真实的项目中经常会有这样的问题而且你绕不开。咱写这一系列文章当然不能别人写什么我就写什么,我喜欢把现实中自己遇到的一些问题都抛出来,为解决问题提供一种思路。当然了,代码肯定不是真实的,是因为我故意为之,想通过一些大家喜闻乐见的案例把思想描绘清楚。如果贴一些项目代码,由于您没有需求背景,反而为学习增加了负担。
总结
本章主要讲解了工厂,不用提它是否能对应统一语言,仅就能简化领域模型的创建你就值得拥有。着重说明一句,工厂是一种可有可无的组件,具体视您的领域模型的复杂度。实践中,基本上一个聚合都会有一个工厂对应的,毕竟能够成为实体的东西其构造过程也简单不了。
附一:本节写得不好,可能是受工作影响比较大,心态不太理想。无论你多么努力与追求上进,面对权力时不得不进行妥协。本来想踏实的做一些东西,奈何树欲静而风不止,可悲。虽说“人有凌云之志非运不能腾达”,不过这个运到底什么时候到来????
附二:本节讨论了工厂的三个模式及在实践中要如何的使用。其中三个模式是要被重点关注的内容,您需要根据不同的情况选择合格的那一种。那么这三个模式到底有什么区别以及最合适的用应用场景是什么呢?大概说明一下。1)实体包含工厂方法:一般不用于构造当前的实体,把自身的创建过程放到实体中会增加实体的复杂性。当你使用了领域事件的时候则强烈推荐这种模式。一般情况下,事件组成的主体信息来自实体的内部,所以由某个方法执行后再创建一个事件更显得更优雅,这个也就是我们常听说的知识专家。2)实体子类作为工厂,如果在实体的创建过程中涉及某些私有方法的调用,这种方式当然更好。再说了,你不想把让实体承担更多的责任又想把实体的创建过程进行封装,怎么着也得建一个工厂对象来完成这个事情,既然继承的方式能让工厂存在更多的扩展能力,为什么不用呢?所以这种方式一般用于实体自身的创建。3)业务服务类作为工厂。首先,这种模式一般用于实体的创建而非反序列化;第二,其创建的实体一般是其它BC的聚合到当前BC的投影,比如上例中说的销售BC中的产品映射到订购BC中的商品。注意,这里的BC是广义的,不仅仅是某个服务,也可能是某个包或名称空间。这类实体的创建,其信息来自外部,你不可能放到某个实体的方法中,也不可能使用继承机制,肯定要使用第三个模式了。