在问题空间中存在不少具备固有身份的概念,一般状况下,这些概念将建模为实体。redis
实体是具备惟一标识的概念,找到领域中的实体并对其进行建模是很是重要的环节。若是理解一个概念是一个实体,就应该追问领域专家相关的细节,好比概念生命周期、核心数据、具体操做、不变规则等;从技术上来讲,咱们能够应用实体相关模式和实践。算法
一个实体是一个惟一的东西,而且能够在至关长的一段时间内持续变化。spring
实体是一个具备身份和连贯性的概念。数据库
一个实体就是一个独立的事物。每一个实体都拥有一个 惟一标识符 (也就是身份),并经过 标识 与和 类型 对实体进行区分开。一般状况下,实体是可变的,也就是说,他的状态随着时间发生变化。后端
惟一身份标识 和 可变性特征 将实体对象和值对象区分开来。设计模式
因为从数据建模出发,一般状况下,CRUD 系统不能建立出好的业务模型。在使用 DDD 的状况下,咱们会将数据模型转化成实体模型。浏览器
从根本上说,实体主要与身份有关,它关注“谁”而非 “什么”。缓存
大多数实体都有相似的特征,所以存在一些设计和实现上的技巧,其中包括惟一标识、属性、行为、验证等。bash
在实体设计早期,咱们刻意将关注点放在能体现实体 惟一性属性 和 行为 上,同时还将关注如何对实体进行查询。网络
有时,实体具备明确的天然标识,能够经过对概念的建模来实现;有时,可能没有已存的天然标识,将由应用程序生成并分配一个合理的标识,并将其用于数据存储。
在考虑实体身份时,首先考虑该实体所在问题空间是否已经存在惟一标识符,这些标识符被称为天然键。
一般状况下,如下几类信息能够做为天然键使用:
在使用时,咱们一般使用值对象模式对天然键进行建模,而后为实体添加一个构造函数,并在构造函数中完成惟一标识的分配。
首先,须要对书籍 ISBN 值对象建模:
@Value
public class ISBN {
private String value;
}
复制代码
而后,对 Book 实体建模:
@Data
public class Book {
private ISBN id;
public Book(ISBN isbn){
this.setId(isbn);
}
public ISBN getId(){
return this.id;
}
private void setId(ISBN id){
Preconditions.checkArgument(id != null);
this.id = id;
}
}
复制代码
Book 在构造函数中完成 id 的赋值,以后便不会修改,以保护实体标识的稳定性。
天然键,在实际研发中,不多使用。特别是在须要用户手工输入的状况下,不免会形成输入错误。对标识的修改会致使引用失效,所以,咱们不多使用用户提供的惟一标识。一般状况下,会将用户输入做为实体属性,这些属性能够用于对象匹配,可是咱们并不将这样的属性做为惟一身份标识。
当问题域中没有惟一标识时,咱们须要决定标识生成策略并生成它。
最多见的生成方式包括自增数值、全局惟一标识符(UUID、GUID等)以及字符串等。
数字一般具备最小的空间占用,很是利于持久化,但须要维护分配 ID 的全局计数器。
咱们可使用全局的静态变量做为全局计数器,如:
public final class NumberGenerator {
private static final AtomicLong ATOMIC_LONG = new AtomicLong(1);
public static Long nextNumber(){
return ATOMIC_LONG.getAndIncrement();
}
}
复制代码
可是,但应用崩溃或重启时,静态变量就会丢失它的值,这意味着会生成重复的 ID,从而致使业务问题。为了纠正这个问题,咱们须要利用全局持久化资源构建计数器。
咱们可使用 Redis 或 DB 构建本身的全局计数器。
基于 Redis inc 指令的全局计数器:
@Component
public class RedisBasedNumberGenerator {
private static final String NUMBER_GENERATOR_KEY = "number-generator";
@Autowired
private RedisTemplate<String, Long> redisTemplate;
public Long nextNumber(){
return this.redisTemplate.boundValueOps(NUMBER_GENERATOR_KEY)
.increment();
}
}
复制代码
基于 DB 乐观锁的全局计数器: 首先,定义用于生成 Number 的表结构:
create table tb_number_gen
(
id bigint auto_increment primary key,
`version` bigint not null,
type varchar(16) not null,
current_number bigint not null
);
create unique index 'unq_type' on tb_number_gen ('type');
复制代码
而后,使用乐观锁完成 Number 生成逻辑:
@Component
public class DBBasedNumberGenerator {
private static final String NUMBER_KEY = "common";
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource){
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Long nextNumber(){
do {
try {
Long number = nextNumber(NUMBER_KEY);
if (number != null){
return number;
}
}catch (Exception e){
// 乐观锁更新失败,进行重试
// LOGGER.error("opt lock failure to generate number, retry ...");
}
}while (true);
}
/**
* 表结构:
* create table tb_number_gen
* (
* id bigint auto_increment primary key,
* `version` bigint not null,
* type varchar(16) not null,
* current_number bigint not null
* );
* add unique index 'unq_type' on tb_number_gen ('type');
*
* @param type
* @return
*/
private Long nextNumber(String type){
NumberGen numberGen = jdbcTemplate.queryForObject(
"select id, type, version, current_number as currentNumber " +
"from tb_number_gen " +
"where type = '" + type +"'",
NumberGen.class);
if (numberGen == null){
// 不存在时,建立新记录
int result = jdbcTemplate.update("insert into tb_number_gen (type, version, current_number) value ('" + type +" ', '0', '1')");
if (result > 0){
return 1L;
}else {
return null;
}
}else {
// 存在时,使用乐观锁 version 更新记录
int result = jdbcTemplate.update("update tb_number_gen " +
"set version = version + 1," +
"current_number = current_number + 1 " +
"where " +
"id = " + numberGen.getId() + " " +
" and " +
"version = " + numberGen.getVersion()
);
// 更新成功,说明从读取到更新这段时间,数据没有发生变化,numberGen 有效,结果为 number + 1
if (result > 0){
return numberGen.getCurrentNumber() + 1;
}else {
// 更新失败,说明从读取到更新这段时间,数据发生变化,numberGen 无效,获取 number 失败
return null;
}
}
}
@Data
class NumberGen{
private Long id;
private String type;
private int version;
private Long currentNumber;
}
}
复制代码
GUID 生成很是方便,而且自身就保障是惟一的,不过在持久化时会占用更多的存储空间。这些额外的空间相对来讲微不足道,所以对大多数应用来讲,GUID 是默认方法。
有不少算法能够生成全局惟一的标识,如 UUID、GUID 等。
生成策略,须要参考不少因子,以产生惟一标识:
但,咱们没有必要本身写算法构建惟一标识。Java 中的 UUID 是一种快速生成惟一标识的方法。
@Component
public class UUIDBasedNumberGenerator {
public String nextId(){
return UUID.randomUUID().toString();
}
}
复制代码
若是对性能有很高要求的场景,能够将 UUID 实例缓存起来,经过后台线程不断的向缓存中添加新的 UUID 实例。
@Component
public class UUIDBasedPoolNumberGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(UUIDBasedPoolNumberGenerator.class);
private final BlockingQueue<String> idQueue = new LinkedBlockingQueue<>(100);
private Thread createThread;
/**
* 直接从队列中获取已经生成的 ID
* @return
*/
public String nextId(){
try {
return idQueue.take();
} catch (InterruptedException e) {
LOGGER.error("failed to take id");
return null;
}
}
/**
* 建立后台线程,生成 ID 并放入到队列中
*/
@PostConstruct
public void init(){
this.createThread = new Thread(new CreateTask());
this.createThread.start();
}
/**
* 销毁线程
*/
@PreDestroy
public void destroy(){
this.createThread.interrupt();
}
/**
* 不停的向队列中放入 UUID
*/
class CreateTask implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
try {
idQueue.put(UUID.randomUUID().toString());
} catch (InterruptedException e) {
LOGGER.error("failed to create uuid");
}
}
}
}
}
复制代码
当在浏览器中建立一个实体并提交回多个后端 API 时,GUID 就会很是有用。若是没有 ID 后端服务将没法对相同实体进行识别。这时,最好使用 JavaScript 在客户端建立一个 GUID 来解决。
在浏览器中生成 GUID,能够有效控制提交数据的幂等性。
字符串经常使用于自定义 ID 格式,好比基于时间戳、多特征组合等。
以下例订单惟一标识:
public class OrderIdUtils {
public static String createOrderId(String day, String owner, Long number){
return String.format("%s-%s-%s", day, owner, number);
}
}
复制代码
一个订单 ID 由日期、全部者和序号三者组成。
对于标识,使用 String 来维护并非很好的方法,没法对其生成策略、具体格式进行有效限制。使用一个值对象会更加合适。
@Value
public class OrderId {
private final String day;
private final String owner;
private final Long number;
public OrderId(String day, String owner, Long number) {
this.day = day;
this.owner = owner;
this.number = number;
}
public String getValue(){
return String.format("%s-%s-%s", getDay(), getOwner(), getNumber());
}
@Override
public String toString(){
return getValue();
}
}
复制代码
相比之下,OrderId 比 String 拥有更强的表达力。
将惟一标识的生成委派给持久化机制是最简单的方案。咱们从数据库获取的序列老是递增,结果老是惟一的。
大多数数据库(如 MySQL)都原生支持 ID 的生成。咱们把新建实体传递到数据访问框架,在事务成功完成后,实体便有了 ID 标识。
一个使用 JPA 持久化的实例以下: 首先,定义 Entity 实体:
@Data
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Date birthAt;
}
复制代码
实体类上添加 @Entity 注解标记为实体;@Id 标记该属性为标识;@GeneratedValue(strategy = GenerationType.IDENTITY) 说明使用数据库自增主键生成方式。 而后,定义 PersonRepository :
public interface PersonRepository extends JpaRepository<Person, Long> {
}
复制代码
PersonRepository 继承于 JpaRepository,具体的实现类会在运行时由 Spring Data Jpa 自动建立,咱们只需直接使用便可。
@Service
public class PersonApplication {
@Autowired
private PersonRepository personRepository;
public Long save(Person person){
this.personRepository.save(person);
return person.getId();
}
}
复制代码
在成功调用 save(person) 后,JPA 框架负责将数据库生成的 ID 绑定到 Person 的 id 属性上,person.getId() 方法便能获取 id 信息。
性能多是这种方法的一个缺点。
经过集成上下文,能够从另外一个限界上下文中获取惟一标识。但通常不会直接使用其余限界上下文的标识,而是须要将其翻译成本地限界上下文的概念。
这也是比较常见的一种策略。例如,在用户成功注册后,系统自动为其生成惟一名片,此时,名片惟一标识即可以直接使用用户 ID。
当用户注册成功后,User 限界上下文将发布 UserRegisteredEvent 事件。
@Value
public class UserRegisteredEvent {
private final UserId userId;
private final String userName;
private final Date birthAt;
}
复制代码
Card 限界上下文,从 MQ 中获取 UserRegisteredEvent 事件,并将 UserId 翻译成本地的 CardId,而后基于 CardId 进行业务处理。具体以下:
@Component
public class UserEventHandler {
@EventListener
public void handle(UserRegisteredEvent event){
UserId userId = event.getUserId();
CardId cardId = new CardId(userId.getValue());
...
}
}
复制代码
实体惟一标识的生成既能够发生在对象建立的时候,也能够发生在持久化对象的时候。
标识生成时间:
在某些状况下,将标识生成延迟到实例持久化会有些问题:
相比之下,及早生成实体标识是比较推荐的作法。
有些 ORM 框架,须要经过本身的方式来处理对象标识。
为了解决这个问题,咱们须要使用两种标识,一种为领域使用,一种为 ORM 使用。这个在 ORM 使用的标识,咱们称为委派标识。
委派标识和领域中的实体标识没有任何关系,委派标识只是为了迎合 ORM 而建立的。 对于外界来讲,咱们最好将委派标识隐藏起来,由于委派标识并非领域模型的一部分,将委派标识暴露给外界可能形成持久化漏洞。
首先,咱们须要定义一个公共父类 IdentitiedObject,用于对委派标识进行集中管理。
@MappedSuperclass
public class IdentitiedObject {
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PRIVATE)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long _id;
}
复制代码
委派标识的 setter 和 getter 都是 private 级别,禁止程序对其进行修改(JPA 框架经过反射对其进行访问)。而后,定义 IdentitiedPerson 实体类:
@Data
@Entity
public class IdentitiedPerson extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private PersonId id;
private String name;
private Date birthAt;
private IdentitiedPerson(){
}
public IdentitiedPerson(PersonId id){
setId(id);
}
}
复制代码
IdentitiedPerson 实体以 PersonId 做为本身的业务标识,而且只能经过构造函数对其进行赋值。这样在隐藏委派标识的同时,完成了业务建模。
领域标识不须要做为数据库的主键,但大多数状况下,须要设置为惟一键。
在聚合边界内,咱们能够将缩短后的标识做为实体的本地标识。而做为聚合根的实体须要全局的惟一标识。
聚合内部实体,只能经过聚合根进行间接访问。所以,只需保障在聚合内部具备惟一性便可。 例如,聚合根 Order 拥有一个 OrderItem 的集合,对于 OrderItem 的访问必须经过 Order 聚合根,所以,OrderItem 只需保障局部惟一便可。
@Value
public class OrderItemId {
private Integer value;
}
@Data
@Entity
public class OrderItem extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private OrderItemId id;
private String productName;
private Integer price;
private Integer count;
private OrderItem(){
}
public OrderItem(OrderItemId id, String productName, Integer price, Integer count){
setId(id);
setProductName(productName);
setPrice(price);
setCount(count);
}
}
复制代码
OrderItemId 为 Integer 类型,由 Order 完成其分配。
@Entity
public class Order extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private OrderId id;
@OneToMany
private List<OrderItem> items = Lists.newArrayList();
public void addItem(String productName, Integer price, Integer count){
OrderItemId itemId = createItemId();
OrderItem item = new OrderItem(itemId, productName, price, count);
this.items.add(item);
}
private OrderItemId createItemId() {
Integer maxId = items.stream()
.mapToInt(item->item.getId().getValue())
.max()
.orElse(0);
return new OrderItemId(maxId + 1);
}
}
复制代码
createItemId 方法获取现有 OrderItem 集合中最大的 id,并经过自增的方式,生成新的 id,从而保证在 Order 范围内的惟一性。相反,聚合根 Order 须要进行全局访问,所以,OrderId 须要全局惟一。
@Value
public class OrderId {
private final String day;
private final String owner;
private final Long number;
public OrderId(String day, String owner, Long number) {
this.day = day;
this.owner = owner;
this.number = number;
}
public String getValue(){
return String.format("%s-%s-%s", getDay(), getOwner(), getNumber());
}
@Override
public String toString(){
return getValue();
}
}
复制代码
实体专一于身份和连续性,若是将过多的职责添加到实体上,容易使实体变的臃肿。一般须要将相关行为委托给值对象和领域服务。
值对象可合并、可比较和自验证,并方便测试。这些特征使其很是适用于承接实体的行为。
在一个分期付款的场景中,咱们须要将总金额按照分期次数进行拆分,若是发生不能整除的状况,将剩下的金额合并到最后一笔中。
@Entity
@Data
public class Loan {
private Money total;
public List<Money> split(int size){
return this.total.split(size);
}
}
复制代码
其中,核心的查分逻辑在值对象 Money 中。
public class Money implements ValueObject {
public static final String DEFAULT_FEE_TYPE = "CNY";
@Column(name = "total_fee")
private Long totalFee;
@Column(name = "fee_type")
private String feeType;
private static final BigDecimal NUM_100 = new BigDecimal(100);
private Money() {
}
private Money(Long totalFee, String feeType) {
Preconditions.checkArgument(totalFee != null);
Preconditions.checkArgument(StringUtils.isNotEmpty(feeType));
Preconditions.checkArgument(totalFee.longValue() > 0);
this.totalFee = totalFee;
this.feeType = feeType;
}
public static Money apply(Long totalFee){
return apply(totalFee, DEFAULT_FEE_TYPE);
}
public static Money apply(Long totalFee, String feeType){
return new Money(totalFee, feeType);
}
private void checkInput(Money money) {
if (money == null){
throw new IllegalArgumentException("input money can not be null");
}
if (!this.getFeeType().equals(money.getFeeType())){
throw new IllegalArgumentException("must be same fee type");
}
}
public List<Money> split(int count){
if (getTotalFee() < count){
throw new IllegalArgumentException("total fee can not lt count");
}
List<Money> result = Lists.newArrayList();
Long pre = getTotalFee() / count;
for (int i=0; i< count; i++){
if (i == count-1){
Long fee = getTotalFee() - (pre * (count - 1));
result.add(Money.apply(fee, getFeeType()));
}else {
result.add(Money.apply(pre, getFeeType()));
}
}
return result;
}
}
复制代码
可见,经过将功能推到值对象,不只避免了实体 Loan 的臃肿,并且经过值对象 Money 的封装,大大增长了重用性。
领域服务没有标识、没有状态,对逻辑进行封装。很是适合承接实体的行为。
咱们看一个秘密加密需求:
@Entity
@Data
public class User {
private String password;
public boolean checkPassword(PasswordEncoder encoder, String pwd){
return encoder.matches(pwd, password);
}
public void changePassword(PasswordEncoder encoder, String pwd){
setPassword(encoder.encode(pwd));
}
}
复制代码
其中 PasswordEncoder 为领域服务
public interface PasswordEncoder {
/**
* 秘密编码
*/
String encode(CharSequence rawPassword);
/**
* 验证密码有效性
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
复制代码
经过将密码加密和验证逻辑推到领域服务,不只下降了实体 User 的臃肿,还可使用策略模式对加密算法进行灵活替换。
实体是业务操做的承载者,行为命名表明着很强的领域概念,须要使用通用语言中的动词,应极力避免 setter 方式的命名规则。
假设,一个新闻存在 上线 和 下线 两个状态。
public enum NewsStatus {
ONLINE, // 上线
OFFLINE; // 下线
}
复制代码
假如直接使用 setter 方法,上线和下线两个业务概念很难表达出来,从而致使概念的丢失。
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
/**
* 直接的 setter 没法表达业务含义
* @param status
*/
public void setStatus(NewsStatus status){
this.status = status;
}
}
复制代码
setStatus 体现的是数据操做,而非业务概念。此时,咱们须要使用具备业务含义的方法命名替代 setter 方法。
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
public void online(){
setStatus(NewsStatus.ONLINE);
}
public void offline(){
setStatus(NewsStatus.OFFLINE);
}
}
复制代码
与 setStatus 不一样,online 和 offline 具备明确的业务含义。
在实体行为成功执行以后,经常须要将变动通知给其余模块或系统,以触发后续流程。所以,须要向外发布领域事件。
发布领域事件,最大的问题是,在实体中如何获取发布事件接口 DomainEventPublisher 。常见的有如下几种模式:
首先,咱们须要定义事件相关接口。
DomainEvent:定义领域事件。
public interface DomainEvent<ID, E extends Entity<ID>> {
E getSource();
default String getType() {
return this.getClass().getSimpleName();
}
}
复制代码
DomainEventPublisher:用于发布领域事件。
public interface DomainEventPublisher {
<ID, EVENT extends DomainEvent> void publish(EVENT event);
default <ID, EVENT extends DomainEvent> void publishAll(List<EVENT> events) {
events.forEach(this::publish);
}
}
复制代码
DomainEventSubscriber: 事件订阅器,用于筛选待处理事件。
public interface DomainEventSubscriber<E extends DomainEvent> {
boolean accept(E e);
}
复制代码
DomainEventHandler: 用于处理领域事件。
public interface DomainEventHandler<E extends DomainEvent> {
void handle(E event);
}
复制代码
DomainEventHandlerRegistry : 对 DomainEventSubscriber 和 DomainEventHandler 注册。
public interface DomainEventHandlerRegistry {
default <E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventHandler<E> handler){
register(subscriber, new DomainEventExecutor.SyncExecutor(), handler);
}
default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventHandler<E> handler){
register(event -> event.getClass() == eventCls, new DomainEventExecutor.SyncExecutor(), handler);
}
default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventExecutor executor, DomainEventHandler<E> handler){
register(event -> event.getClass() == eventCls, executor, handler);
}
<E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventExecutor executor, DomainEventHandler<E> handler);
<E extends DomainEvent> void unregister(DomainEventSubscriber<E> subscriber);
<E extends DomainEvent> void unregisterAll(DomainEventHandler<E> handler);
}
复制代码
DomainEventBus: 继承自 DomainEventPublisher 和 DomainEventHandlerRegistry, 提供事件发布和订阅功能。
public interface DomainEventBus extends DomainEventPublisher, DomainEventHandlerRegistry{
}
复制代码
DomainEventExecutor: 事件执行器,指定事件执行策略。
public interface DomainEventExecutor {
Logger LOGGER = LoggerFactory.getLogger(DomainEventExecutor.class);
default <E extends DomainEvent> void submit(DomainEventHandler<E> handler, E event){
submit(new Task<>(handler, event));
}
<E extends DomainEvent> void submit(Task<E> task);
@Value
class Task<E extends DomainEvent> implements Runnable{
private final DomainEventHandler<E> handler;
private final E event;
@Override
public void run() {
try {
this.handler.handle(this.event);
}catch (Exception e){
LOGGER.error("failed to handle event {} use {}", this.event, this.handler, e);
}
}
}
class SyncExecutor implements DomainEventExecutor{
@Override
public <E extends DomainEvent> void submit(Task<E> task) {
task.run();
}
}
}
复制代码
做为业务方法的参数进行传递 是最简单的策略,具体以下:
public class Account extends JpaAggregate {
public void enable(DomainEventPublisher publisher){
AccountEnabledEvent event = new AccountEnabledEvent(this);
publisher.publish(event);
}
}
复制代码
这种实现方案虽然简单,可是很琐碎,每次都须要传递 DomainEventPublisher 参数,无形中提升了调用方的复杂性。
经过 ThreadLocal 与线程绑定 将 EventPublisher 绑定到线程上下文中,在使用时,直接经过静态方法获取并进行事件发布。
public class Account extends JpaAggregate {
public void enable(){
AccountEnabledEvent event = new AccountEnabledEvent(this);
DomainEventPublisherHolder.getPubliser().publish(event);
}
}
复制代码
DomainEventPublisherHolder 实现以下:
public class DomainEventPublisherHolder {
private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){
@Override
protected DomainEventBus initialValue() {
return new DefaultDomainEventBus();
}
};
public static DomainEventPublisher getPubliser(){
return THREAD_LOCAL.get();
}
public static DomainEventHandlerRegistry getHandlerRegistry(){
return THREAD_LOCAL.get();
}
}
复制代码
将事件暂存在实体 是比较推荐的方法,具备很大的灵活性。
public class Account extends JpaAggregate {
public void enable(){
AccountEnabledEvent event = new AccountEnabledEvent(this);
registerEvent(event);
}
}
复制代码
registerEvent 方法在 AbstractAggregate 类中,将 Event 暂存到 events 中。
@MappedSuperclass
public abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class);
@JsonIgnore
@QueryTransient
@Transient
@org.springframework.data.annotation.Transient
private final transient List<DomainEventItem> events = Lists.newArrayList();
protected void registerEvent(DomainEvent event) {
events.add(new DomainEventItem(event));
}
protected void registerEvent(Supplier<DomainEvent> eventSupplier) {
this.events.add(new DomainEventItem(eventSupplier));
}
@Override
@JsonIgnore
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events.stream()
.map(eventSupplier -> eventSupplier.getEvent())
.collect(Collectors.toList()));
}
@Override
public void cleanEvents() {
events.clear();
}
private class DomainEventItem {
DomainEventItem(DomainEvent event) {
Preconditions.checkArgument(event != null);
this.domainEvent = event;
}
DomainEventItem(Supplier<DomainEvent> supplier) {
Preconditions.checkArgument(supplier != null);
this.domainEventSupplier = supplier;
}
private DomainEvent domainEvent;
private Supplier<DomainEvent> domainEventSupplier;
public DomainEvent getEvent() {
if (domainEvent != null) {
return domainEvent;
}
DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null;
domainEvent = event;
return domainEvent;
}
}
}
复制代码
完成暂存后,在成功持久化后,进行事件发布。
// 持久化实体
this.aggregateRepository.save(a);
if (this.eventPublisher != null){
// 对实体中保存的事件进行发布
this.eventPublisher.publishAll(a.getEvents());
// 清理事件
a.cleanEvents();
}
复制代码
跟踪变化最实用的方法是领域事件和事件存储。当命令操做执行完成后,系统发出领域事件。事件的订阅者能够接收发生在模型上的事件,在接收事件后,订阅方将事件保存在事件存储中。
变化跟踪,一般与事件存储一并使用,稍后详解。
除了身份标识外,使用实体的一个重要需求是保证他们是自验证,并老是有效的。尽管实体具备生命周期,其状态不断变化,咱们须要保证在整个变化过程当中,实体老是有效的。
验证的主要目的在于检查实体的正确性,检查对象能够是某个属性,也能够是整个对象,甚至是多个对象的组合。
即使领域对象的各个属性都是合法的,也不能表示该对象做为一个总体是合法的;一样,单个对象合法也并不能保证对象组合是合法的。
可使用自封装来验证属性。
自封装性要求不管以哪一种方式访问数据,即便从对象内部访问数据,都必须经过 getter 和 setter 方法。 通常状况下,咱们能够在 setter 方法中,对属性进行合法性验证,好比是否为空、字符长度是否符合要求、邮箱格式是否正确等。
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
public Person(){
}
public Person(String name, Date birthDay) {
setName(name);
setBirthDay(birthDay);
}
public String getName() {
return name;
}
public void setName(String name) {
// 对输入参数进行验证
Preconditions.checkArgument(StringUtils.isNotEmpty(name));
this.name = name;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
// 对输入参数进行验证
Preconditions.checkArgument(birthDay != null);
this.birthDay = birthDay;
}
}
复制代码
在构造函数中,我也仍需调用 setter 方法完成属性赋值。
要验证整个实体,咱们须要访问整个对象的状态----全部对象属性。
验证整个对象,主要用于保证明体知足不变性条件。不变条件来源于明确的业务规则,每每须要获取对象的整个状态以完成验证。
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
@Override
public void validate(ValidationHandler handler){
if (StringUtils.isEmpty(getName())){
handler.handleError("Name can not be empty");
}
if (getBirthDay() == null){
handler.handleError("BirthDay can not be null");
}
}
}
复制代码
其中 ValidationHandler 用于收集全部的验证信息。
public interface ValidationHandler {
void handleError(String msg);
}
复制代码
有时候,验证逻辑比领域对象自己的变化还快,将验证逻辑嵌入在领域对象中会使领域对象承担太多的职责。此时,咱们能够建立一个单独的组件来完成模型验证。在 Java 中设计单独的验证类时,咱们能够将该类放在和实体一样的包中,将属性的 getter 方法生命为包级别,这样验证类便能访问这些属性了。
假如,咱们不想将验证逻辑所有放在 Person 实体中。能够新建 PersonValidator:
public class PersonValidator implements Validator {
private final Person person;
public PersonValidator(Person person) {
this.person = person;
}
@Override
public void validate(ValidationHandler handler) {
if (StringUtils.isEmpty(this.person.getName())){
handler.handleError("Name can not be empty");
}
if (this.person.getBirthDay() == null){
handler.handleError("BirthDay can not be null");
}
}
}
复制代码
而后,在 Person 中调用 PersonValidator:
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
@Override
public void validate(ValidationHandler handler){
new PersonValidator(this).validate(handler);
}
}
复制代码
这样将最大限度的避免 Person 的臃肿。
相比之下,验证对象组合会复杂不少,也比较少见。最经常使用的方式是把这种验证过程建立成一个领域服务。
领域服务,咱们稍后详解。
实体应该面向行为,这意味着实体应该公开领域行为,而不是公开状态。
专一于实体行为很是重要,它使得领域模型更具表现力。经过对象的封装特性,其状态只能被封装它的实例进行操做,这意味着任何修改状态的行为都属于实体。
专一于实体行为,须要谨慎公开 setter 和 getter 方法。特别是 setter 方法,一旦公开将使状态更改直接暴露给用户,从而绕过领域概念直接对状态进行更新。
典型的仍是 News 上下线案例。
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
public void online(){
setStatus(NewsStatus.ONLINE);
}
public void offline(){
setStatus(NewsStatus.OFFLINE);
}
/**
* 直接的 setter 没法表达业务含义
* @param status
*/
private void setStatus(NewsStatus status){
this.status = status;
}
}
复制代码
当咱们新建一个实体时,但愿经过构造函数来初始化足够多的状态。这样,一方面有助于代表该实体的身份,另外一方面能够帮助客户端更容易的查找该实体。
若是实体的不变条件要求该实体所包含的对象不能为 null,或者由其余状态计算所得,那么这些状态须要做为参数传递给构造函数。构造函数对实体变量赋值时,它把操做委派给实例变量的 setter 方法,这样便保证了实体变量的自封装性。
见 Person 实例,将无参构造函数设为 private,以服务于框架;经过 public 暴露全部参数的构造函数,并调用 setter 方法对实体有效性进行验证。
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
private Person(){
}
public Person(String name, Date birthDay) {
setName(name);
setBirthDay(birthDay);
}
public String getName() {
return name;
}
public void setName(String name) {
// 对输入参数进行验证
Preconditions.checkArgument(StringUtils.isNotEmpty(name));
this.name = name;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
// 对输入参数进行验证
Preconditions.checkArgument(birthDay != null);
this.birthDay = birthDay;
}
}
复制代码
对于使用一个实体承载多个类型的场景,咱们可使用实体上的静态方法,对不一样类型进行不一样构建。
@Setter(AccessLevel.PRIVATE)
@Entity
public class BaseUser extends JpaAggregate {
private UserType type;
private String name;
private BaseUser(){
}
public static BaseUser createTeacher(String name){
BaseUser baseUser = new BaseUser();
baseUser.setType(UserType.TEACHER);
baseUser.setName(name);
return baseUser;
}
public static BaseUser createStudent(String name){
BaseUser baseUser = new BaseUser();
baseUser.setType(UserType.STUDENT);
baseUser.setName(name);
return baseUser;
}
}
复制代码
相对,构造函数,静态方法 createTeacher 和 createStudent 具备更多的业务含义。
对于那些很是复杂的建立实体的状况,咱们可使用工厂模式。
这个不只限于实体,对于复杂的实体、值对象、聚合均可应用工厂。而且,此处所说的工厂,也不只限于工厂模式,也可使用 Builder 模式。总之,就是将复杂对象的建立与对象自己功能进行分离,从而完成对象的瘦身。
分布式系已经成为新的标准,咱们须要在新标准下,思考对领域设计的影响。
强烈建议不要分布单个实体。在本质上,这意味着一个实体应该被限制成单个有界上下文内部的单个领域模型中的单个类(或一组类)。
假如,咱们将单实体的不一样部分分布在一个分布式系统之上。为了实现实体的一致性,可能须要全局事务保障,大大增长了系统的复杂度。要加载这个实体的话,查询多个不一样系统也是一种必然。分布式系统中的网络开销将会放大,从而致使严重的性能问题。
上图,将 OrderItem 和 ProductInfo 与 Order 进行分布式部署,在获取 Oder 时会致使大量的 RPC 调用,下降系统性能。
正确的部分方案为:
对于多个实体间,进行分布式部署,能够将压力进行分散,大大增长系统性能。
这种部署方式是推荐方式。
建模模式有利于提高实体的表达性和可维护性。
惟一标识是实体的身份,在完成分配后,绝对不容许修改。
对于程序生成:
@Data
public class Book {
private ISBN id;
private Book(){
}
public Book(ISBN isbn){
this.setId(isbn);
}
public ISBN getId(){
return this.id;
}
private void setId(ISBN id){
Preconditions.checkArgument(id != null);
this.id = id;
}
}
复制代码
由构造函数传入 id,并将 setter 方法设置为私有,以免被改变。
对于持久化生成:
@Data
@MappedSuperclass
public abstract class JpaAggregate extends AbstractAggregate<Long> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Setter(AccessLevel.PRIVATE)
@Column(name = "id")
private Long id;
@Override
public Long getId() {
return id;
}
}
复制代码
使用 private 属性和 setter 方法,避免被修改。同时提供 public 的 getter 方法,用于获取生成的 id。
Specification 也称规格模式,主要针对领域模型中的描述规格进行建模。
规范模式是一种软件设计模式,可用于封装定义所需对象状态的业务规则。这是一种很是强大的方法,能够减小耦合并提升可扩展性,以选择与特定条件匹配的对象子集。这些规格可使用逻辑运算符组合,从而造成复合规范。
规格 Specification 模式是将一段领域知识封装到一个单元中,称为规格。而后,在不一样的场景中重用。主要有三种这样的场景:
这颇有用,由于它容许你避免域知识重复。当向用户显示数据时,相同的规格类可用于验证传入数据和从数据库中过滤数据。
在了解完 Specification 的特征 后,咱们须要一个框架,它提供了 Specification 相关 API,既能从存储中检索数据,也能对内存对象进行验证。
在这,咱们使用 Querydsl 进行构建。
一个 News 实体,存在两种状态,一个是用户本身设置的 NewsStatus,用于标记当前是上线仍是下线状态;一个是管理员设置的 NewsAuditStatus,用于标记当前是审核经过仍是审核拒绝状态。只有在用户设置为上线同时管理员审核经过,该 News 才可显示。
首先,咱们先定义规则。
public class NewsPredicates {
/**
* 获取可显示规则
* @return
*/
public static PredicateWrapper<News> display(){
return new Display();
}
/**
* 可显示规则
*/
static class Display extends AbstractPredicateWrapper<News>{
protected Display() {
super(QNews.news);
}
@Override
public Predicate getPredicate() {
Predicate online = QNews.news.status.eq(NewsStatus.ONLINE);
Predicate passed = QNews.news.auditStatus.eq(NewsAuditStatus.PAASED);
return new BooleanBuilder()
.and(online)
.and(passed);
}
}
}
复制代码
该规则能够应用于内存对象。
@Entity
@Data
@QueryEntity
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsAuditStatus auditStatus;
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
/**
* 判断是不是可显示的
* @return
*/
public boolean isDisplay(){
return NewsPredicates.display().accept(this);
}
}
复制代码
同时,该规则也能够用于数据检索。
public interface NewsRepository extends Repository<News, Long>, QuerydslPredicateExecutor<News> {
/**
* 查找可显示的信息
* @param pageable
* @return
*/
default Page<News> getDispaly(Pageable pageable){
return findAll(NewsPredicates.display().getPredicate(), pageable);
}
}
复制代码
可显示规则所有封装于 NewsPredicates 中,若是规则发生变化,只需对 NewsPredicates 进行调整便可。
实体拥有本身的生命周期,每每会涉及状态管理。对状态建模是实体建模的重要部分。
管理实体状态,状态设计模式具备很大的诱惑。
好比一个简单的审核流程。
graph TB
已提交--经过-->审核经过
已提交--修改-->已提交
已提交--拒绝-->审核拒绝
审核拒绝--修改-->已提交
复制代码
使用状态模式以下:
首先,定义状态接口。
public interface AuditStatus {
AuditStatus pass();
AuditStatus reject();
AuditStatus edit();
}
复制代码
该接口中包含全部操做。而后,定义异常类。
public class StatusNotSupportedException extends RuntimeException{
}
复制代码
在当前状态不容许执行某些操做时,直接抛出异常,以中断流程。而后,定义各个状态类,以下:
SubmittedStatus
public class SubmittedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
return new PassedStatus();
}
@Override
public AuditStatus reject() {
return new RejectedStatus();
}
@Override
public AuditStatus edit() {
return new SubmittedStatus();
}
}
复制代码
PassedStatus
public class PassedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus reject() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus edit() {
throw new StatusNotSupportedException();
}
}
复制代码
RejectedStatus
public class RejectedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus reject() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus edit() {
return new SubmittedStatus();
}
}
复制代码
但,状态模式致使大量的模板代码,对于简单业务场景显得有些冗余。同时太多的状态类为持久化形成了很多麻烦。此时,咱们可使用 Enum 对其进行简化。
public enum AuditStatusEnum {
SUBMITED(){
@Override
public AuditStatusEnum pass() {
return PASSED;
}
@Override
public AuditStatusEnum reject() {
return REJECTED;
}
@Override
public AuditStatusEnum edit() {
return SUBMITED;
}
},
PASSED(){
},
REJECTED(){
@Override
public AuditStatusEnum edit() {
return SUBMITED;
}
};
public AuditStatusEnum pass(){
throw new StatusNotSupportedException();
}
public AuditStatusEnum reject(){
throw new StatusNotSupportedException();
}
public AuditStatusEnum edit(){
throw new StatusNotSupportedException();
}
}
复制代码
AuditStatusEnum 与 以前的状态模式功能彻底一致,但代码要紧凑的多。
另外,使用显示建模也是一种解决方案。这种方式会为每一个状态建立一个类,经过类型检测机制严格控制能操做的方法,但对于存储有些不大友好,在实际开发中,使用的很少。
以前提过,实体不该该绕过业务方法,直接使用 setter 对状态进行修改。
若是业务方法拥有过长的参数列表,在使用上也会致使必定的混淆。最多见策略是,使用 DTO 对业务所需数据进行传递,并在业务方法中调用 getter 方法获取对于数据。
@Entity
@Data
public class User {
private String name;
private String nickName;
private Email email;
private Mobile mobile;
private Date birthDay;
private String password;
public boolean checkPassword(PasswordEncoder encoder, String pwd){
return encoder.matches(pwd, password);
}
public void changePassword(PasswordEncoder encoder, String pwd){
setPassword(encoder.encode(pwd));
}
public void update(String name, String nickName, Email email, Mobile mobile, Date birthDay){
setName(name);
setNickName(nickName);
setEmail(email);
setMobile(mobile);
setBirthDay(birthDay);
}
public void update(UserDto userDto){
setName(userDto.getName());
setNickName(userDto.getNickName());
setEmail(userDto.getEmail());
setMobile(userDto.getMobile());
setBirthDay(userDto.getBirthDay());
}
}
复制代码
实体存储的数据,每每须要读取出来,在 UI 中显示,或被其余系统使用。
实体做为领域概念,不容许脱离领域层,而在 UI 中直接使用。此时,咱们须要使用备忘录或 DTO 模式,将实体与数据解耦。
方法的反作用,是指一个方法的执行,若是在返回一个值以外还致使某些“状态”发生变化,则称该方法产生了反作用。
根据反作用概念,咱们能够提取出两类方法:
在实际开发中,须要对二者进行严格区分。
在 Application 中,Command 方法须要开启写事务;Query 方法只需开启读事务便可。
@Service
public class NewsApplication extends AbstractApplication {
@Autowired
private NewsRepository repository;
@Transactional(readOnly = false)
public Long createNews(String title, String content){
return creatorFor(this.repository)
.instance(()-> News.create(title, content))
.call()
.getId();
}
@Transactional(readOnly = false)
public void online(Long id){
updaterFor(this.repository)
.id(id)
.update(News::online)
.call();
}
@Transactional(readOnly = false)
public void offline(Long id){
updaterFor(this.repository)
.id(id)
.update(News::offline)
.call();
}
@Transactional(readOnly = false)
public void reject(Long id){
updaterFor(this.repository)
.id(id)
.update(News::reject)
.call();
}
@Transactional(readOnly = false)
public void pass(Long id){
updaterFor(this.repository)
.id(id)
.update(News::pass)
.call();
}
@Transactional(readOnly = true)
public Optional<News> getById(Long id){
return this.repository.getById(id);
}
@Transactional(readOnly = true)
public Page<News> getDisplay(Pageable pageable){
return this.repository.getDispaly(pageable);
}
}
复制代码
其中,有一个比较特殊的方法,建立方法,因为采用的是数据库生成主键策略,须要将生成的主键返回。
实体主要职责是维护业务不变性,当多个用户同时修改一个实体时,会将事情复杂化,从而致使业务规则的破坏。
对此,须要在实体上使用乐观锁进行并发控制,保障只有一个用户更新成功,从而保护业务不变性。
Jpa 框架自身便提供了对乐观锁的支持,只需添加 @Version 字段便可。
@Getter(AccessLevel.PUBLIC)
@MappedSuperclass
public abstract class AbstractEntity<ID> implements Entity<ID> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractEntity.class);
@Version
@Setter(AccessLevel.PRIVATE)
@Column(name = "version", nullable = false)
private int version;
}
复制代码