2

2. 落地战术设计

落地实体

实体的唯一性由唯一标识确定。标识的生成策略大概有以下几种

  1. 用户提供唯一标识
  2. 程序生成唯一标识
  3. 持久化机制生成唯一标识
  4. 另一个限界上下文提供唯一标识

如果你的实体标识策略不是由持久化机制生成的唯一标识。那么应该把委派标识(为了迎合ORM而建的标识)对外隐藏,委派标识不属于实体的一部分。可以使用层超模式

/**
 * jpa实体通用字段组成的父类
 *
 * @author mazhenjie
 * @since 2019-11-01
 */
@MappedSuperclass
@Getter
@Setter
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseJpaEntity {

    /**
     * id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 创建时间
     */
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    @Column(name = "date_create", updatable = false)
    private Date dateCreate;

    /**
     * 更新时间
     */
    @Temporal(TemporalType.TIMESTAMP)
    @LastModifiedDate
    @Column(name = "date_update")
    private Date dateUpdate;

}

落地值对象

值对象的唯一性是通过属性值去判断的。在建模时我们应该尽可能的将模型建为值对象,使我们更少的进行职责假设。

持久化机制不应该影响到值对象的建模,值对象的持久化方式:

  1. ORM与单个值对象

实体和值对象一对一映射,值对象的属性作为字段存在和实体同一张表中

  1. 多个值对象序列化到单个列中

实体引用了List和Set属性的值对象集合

  1. 使用数据库实体保存多个值对象

值对象单独一个数据库实体表存储,并且带有一个委派主键标识,这个标识不对客户端展示。领域模型依然是一个值对象。持久化相关的逻辑没有泄漏到模型或客户端上去。

持久化机制我是这么感觉的,如果这个不会有对这个值对象里面的属性频繁查找的情况,是可以把这个值对象序列化存入实体表的一个字段中的。如果整个值对象的查找也不是很频繁,也可以选择把这个值对象单独存一张表。如果有对值对象里属性频繁查找的情况,我们可以在实体表上直接存值对象的字段。

public class FulfillmentDemand extends BaseJpaEntity implements Entity<FulfillmentDemand> {

    /**
     * 需求标识
     */
    @Column(name = "demand_id")
    private String demandId;

    /**
     * 订单信息
     */
    @Embedded
    private Order order;
    
    /**
     * 客户信息
     */
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "demand_id", referencedColumnName = "demand_id", insertable = false, updatable = false)
    private Buyer buyer;

上面的订单信息里面的字段就直接存入实体表字段中,客户信息就另外建一个表。对聚合来说都是一样的。

落地聚合、聚合根

聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。一个聚合包含聚合根、实体和值对象。

聚合设计的原则

  • 在一致性边界之内建模真正的不变条件

    1. 一致性。事务一致性、最终一致性。一个事务中只修改一个聚合,反之:不能在一个事务中同时修改多个聚合实例,真要这么做的话要考虑最终一致性
    2. 不变条件。指的是一个业务规则,该规则应该总是保持一致的
  • 设计小聚合。根实体表示聚合,绝大多数根实体可以设计为聚合
  • 通过唯一标识引用其它聚合
  • 在边界之外使用最终一致性
@AggregateRoot
public class FulfillmentRule extends BaseJpaEntity implements Entity<FulfillmentRule> 

聚合根本质还是一个实体,所以我们还是实现实体的接口,单独用个注解标注一下就好

聚合根、实体、值对象区分

从标识角度:聚合根是实体,具有全局的唯一标识。而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,通过属性判断相等性,实现Equals方法。

从是否只读的角度:聚合根除了唯一标识外,其他所有状态信息都理论上可变。实体是可变的。值对象不可变,是只读的。

从生命周期角度:聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体完全由其所属的聚合根负责管理维护。值对象无生命周期可言,因为只是一个值。

聚合根、实体、值对象对象之间如何建立关联

聚合根到聚合根:通过ID关联;

聚合根到其内部的实体,直接对象引用;

聚合根到值对象,直接对象引用;

实体对其他对象的引用规则:

  1. 能引用其所属聚合内的聚合根、实体、值对象。
  2. 能引用外部聚合根,但推荐以ID的方式关联,另外也可以关联某个外部聚合内的实体,但必须是ID关联,否则就出现同一个实体的引用被两个聚合根持有,这是不允许的,一个实体的引用只能被其所属的聚合根持有。

值对象对其他对象的引用规则:只需确保值对象是只读的即可,推荐值对象的所有属性都尽量是值对象。

落地工厂

领域模型中的工厂
  • 将创建复杂对象和聚合的职责分配给一个单独的对象,它并不承担领域模型中的职责,但是领域设计的一部份
  • 对于聚合来说,我们应该一次性的创建整个聚合,并且确保它的不变条件得到满足
  • 工厂只承担创建模型的工作,不具有其它领域行为
聚合根中的工厂方法
  • 聚合根中的工厂方法表现出了领域概念
  • 工厂方法可以提供守卫措施
  • 一个含有工厂方法的聚合根的主要职责是完成它的聚合行为
  • 在聚合上使用工厂方法能更好的表达通用语言,这是使用构造函数所不能表达的
    private ServiceTask(String demandId, String ruleCode, List<ServiceTaskEnum> nodeRules) {
        this.demandId = demandId;
        this.ruleCode = ruleCode;
        //....
    }

    public static ServiceTask initOnTriggerProcess(String demandId, String ruleCode, List<ServiceTaskEnum> nodeRules) {
        return new ServiceTask(demandId, ruleCode, nodeRules);
    }

落地资源库

首先聚合和资源库之间是一一对应的关系。资源库只是一种持久化的手段,不应该包含任何业务操作。

第一步,定义资源库接口,接口中有put或save类似的方法

与面向集合的资源库的不同点:面向集合的资源库只有在新增时调用add即可,面向持久化的无论是新增还是修改都要调用save

实现类放在基础设施层,将领域的概念与持久化相关的概念相分离,依赖倒置原则。基础设施层位与所有层之上,并且单向向下引用领域层

public interface BaseRepository<AGGREGATE, ID extends Serializable> {

    /**
     * 删除
     *
     * @param id
     */
    void delete(ID id);

    /**
     * load聚合根
     *
     * @param id
     * @return
     */
    AGGREGATE load(ID id);

    /**
     * 保存或更新聚合根
     *
     * @param aggregate
     * @param <S>
     * @return
     */
    <S extends AGGREGATE> S save(S aggregate);
}

落地应用服务

应用服务是用来表达用例和用户故事(User Story)的主要手段。

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

应用层作为展现层与领域层的桥梁。展现层使用VO(视图模型)进行界面展示,与应用层通过DTO(数据传输对象)进行数据交互,从而达到展现层与DO(领域对象)解耦的目的。

落地领域服务

个人感觉最理想的情况是没有领域服务,如果领域服务使用不恰当慢慢又演化回了以前逻辑都在service层的局面。可以使用领域服务的情况:

  • 执行一个显著的业务操作
  • 对领域对象进行转换
  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

落地领域事件

  • 根据限界上下文的通用语言来命名事件及其属性
  • 如果事件由聚合上的命令操作产生,则应该根据操作方法的名字来命名领域事件
  • 事件的名字应该反映过去发生的事情
  • 领域事件应该都有一个发生时间属性,同时要包括另外的属性:比哪些聚合跟此事件相关,继承统一的DomainEvent接口
  • 事件所带的属性能够反映出该事件的来源。事件对象提供getter方法。事件属性应该是只读的,没有setter方法
  • 是否有必要消除事件的重复提交
  • 一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作。当一个聚合依赖另一个聚合时,可以通过事件实现它们状态的最终一致性

建模领域事件

推荐命名为Domain Name + 动词的过去式 + Event。这样比较可以确切的表达业务语义。


/**
 * 领域事件基类
 *
 * @author mazhenjie
 * @since 2019-11-05
 */
@Getter
public abstract class BaseDomainEvent<T> implements Serializable {

    private static final long serialVersionUID = 1465328245048581896L;

    private String demandId;

    private Date occurredOn;

    private T data;

    private DomainEventEnum eventType;

    public BaseDomainEvent(String demandId, T data, DomainEventEnum eventType) {
        this.demandId = demandId;
        this.data = data;
        this.eventType = eventType;
        this.occurredOn = new Date();
    }
}

事件的技术实现我们可以选择:

  • spring event、guava event本地事件
  • 发到mq上自己在监听

本地事件的话好处是可以较为自由的管理,坏处是会增加你编码的复杂度。要自己实现持久化、异常处理、至少消费一次机制等

@Aspect
@Component
@Slf4j
public class DomainEventStoreAspect {

    @Autowired
    private DomainEventRepository domainEventRepository;

    @AfterReturning(returning = "result", pointcut = "@annotation(org.springframework.transaction.event.TransactionalEventListener)")
    public void handleFinish(JoinPoint joinPoint, InnerResult result) {
        BaseDomainEvent event = (BaseDomainEvent) joinPoint.getArgs()[0];
        DomainEventEntity domainEventEntity = DomainEventEntity.buildByEvent(event);
        if (!result.isSuccess()) {
            log.error("事件处理异常:{},ex:{}", joinPoint.toString(), result.getMsg());
            domainEventEntity.markError();
        }

        domainEventRepository.saveAndFlush(domainEventEntity);
    }

}

发到mq上的话好处是可靠性完全交给中间件去保证,坏处的话是不方便自己管理,占用公共资源。


夙梦流尘
627 声望42 粉丝

按摩最专业的java程序猿