使用领域事件来捕获发生在领域中的一些事情。

领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域。这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得。

领域事件,可以用于一个限界上下文内的领域模型,也可以使用消息队列在限界上下文间进行异步通信。

1 理解领域事件

领域事件是领域专家所关心的发生在领域中的一些事件。

将领域中所发生的活动建模成一系列离散事件。每个事件都用领域对象表示。领域事件是领域模型的组成部分,表示领域中所发生的事情。

领域事件的主要用途:

  • 保证聚合间的数据一致性
  • 替换批量处理
  • 实现事件源模式
  • 进行限界上下文集成

2 实现领域事件

领域事件表示已经发生的某种事实,该事实在发生后便不会改变。因此,领域事件通常建模成值对象。

但,这也有特殊的情况,为了迎合序列化和反序列化框架需求,在建模时,经常会进行一定的妥协。

2.1 创建领域事件

2.1.1 事件命名

在建模领域事件时,我们应该根据限界上下文中的通用语言来命名事件。

如果事件由聚合上的命令操作产生,通常根据该操作方法的名字来命名事件。事件名字表明聚合上的命令方法在执行成功后的事实。即事件命名需要反映过去发生过的事情。

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {public AccountEnabledEvent(Account source) {super(source);}
}
复制代码
2.1.2 事件属性

事件的属性主要用于驱动后续业务流程。当然,也会拥有一些通用属性。

事件具有一些通用属性,如:

  • 唯一标识
  • occurredOn 发生时间
  • type 事件类型
  • source 事件发生源(只针对由聚合产生的事件)

通用属性可以使用事件接口来规范。

接口或类 含义
DomainEvent 通用领域事件接口
AggregateEvent 由聚合发布的通用领域事件接口
AbstractDomainEvent DomainEvent 实现类,维护 id 和 创建时间
AbstractAggregateEvent AggregateEvent 实现类,继承子 AbstractDomainEvent,并添加 source 属性

但,事件最主要的还是业务属性。我们需要考虑,是谁导致事件的发生,这可能涉及产生事件的聚合或其他参与该操作的聚合,也可能是其他任何类型的操作数据。

2.1.3 事件方法

事件是事实的描述,本身不会有太多的业务操作。

领域事件通常被设计为不变对象,事件所携带的数据已经反映出该事件的来源。事件构造函数完成状态初始化,同时提供属性的 getter 方法。

2.1.4 事件唯一标识

这里需要注意的是事件唯一标识,通常情况下,事件是不可变的,那为什么会涉及唯一标识的概念呢?

对于从聚合中发布出来的领域事件,使用事件的名称、产生事件的标识、事件发生的时间等足以对不同的事件进行区分。但,这样会增加事件比较的复杂性。

对于由调用方发布的事件,我们将领域事件建模成聚合,可以直接使用聚合的唯一标识作为事件的标识。

事件唯一标识的引入,会大大减少事件比较的复杂性。但,其最大的意义在于限界上下文的集成。

当我们需要将领域事件发布到外部的限界上下文时,唯一标识就是一种必然。为了保证事件投递的幂等性,在发送端,我们可能会进行多次发送尝试,直至明确发送成功为止;而在接收端,当接收到事件后,需要对事件进行重复性检测,以保障事件处理的幂等性。此时,事件的唯一标识便可以作为事件去重的依据。

事件唯一标识,本身对领域建模影响不大,但对技术处理好处巨大。因此,将它作为通用属性进行管理。

2.2 发布领域事件

我们如何避免领域事件与处理者间的耦合呢?

一种简单高效的方式便是使用观察者模式,这种模式可以在领域事件和外部组件间进行解耦。

2.2.1 发布订阅模型

为了统一,我们需要定义了一套接口和实现类,以基于观察者模式,完成事件的发布。

涉及接口和实现类如下:

接口或类 含义
DomainEventPublisher 用于发布领域事件
DomainEventHandlerRegistry 用于注册 DomainEventHandler
DomainEventBus 扩展自 DomainEventPublisher 和 DomainEventHandlerRegistry 用于发布和管理领域事件处理器
DefaultDomainEventBus DomainEventBus 默认实现
DomainEventHandler 用于处理领域事件
DomainEventSubscriber 用于判断是否接受领域事件
DomainEventExecutor 用于执行领域事件处理器

使用实例如 DomainEventBusTest 所示:

public class DomainEventBusTest {private DomainEventBus domainEventBus;@Beforepublic void setUp() throws Exception {this.domainEventBus = new DefaultDomainEventBus();}@Afterpublic void tearDown() throws Exception {this.domainEventBus = null;}@Testpublic void publishTest(){// 创建事件处理器TestEventHandler eventHandler = new TestEventHandler();// 注册事件处理器this.domainEventBus.register(TestEvent.class, eventHandler);// 发布事件this.domainEventBus.publish(new TestEvent("123"));// 检测事件处理器是够运行Assert.assertEquals("123", eventHandler.data);}@Valueclass TestEvent extends AbstractDomainEvent{private String data;}class TestEventHandler implements DomainEventHandler<TestEvent>{private String data;@Overridepublic void handle(TestEvent event) {this.data = event.getData();}}
}
复制代码

在构建完发布订阅结构后,需要将其与领域模型进行关联。领域模型如何获取 Publisher,事件处理器如何进行订阅。

2.2.2 基于 ThreadLocal 的事件发布

比较常用的方案便是将 DomainEventBus 绑定到线程上下文。这样,只要是同一调用线程都可以方便的获取 DomainEventBus 对象。

具体的交互如下:

DomainEventBusHolder 用于管理 DomainEventBus。

public class DomainEventBusHolder {private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){@Overrideprotected DomainEventBus initialValue() {return new DefaultDomainEventBus();}};public static DomainEventPublisher getPubliser(){return THREAD_LOCAL.get();}public static DomainEventHandlerRegistry getHandlerRegistry(){return THREAD_LOCAL.get();}public static void clean(){THREAD_LOCAL.remove();}
}
复制代码

Account 的 enable 直接使用 DomainEventBusHolder 进行发布。

public class Account extends JpaAggregate {public void enable(){AccountEnabledEvent event = new AccountEnabledEvent(this);DomainEventBusHolder.getPubliser().publish(event);}
}public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {public AccountEnabledEvent(Account source) {super(source);}
}
复制代码

AccountApplication 完成订阅器注册以及业务方法调用。

public class AccountApplication extends AbstractApplication {private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);@Autowiredprivate AccountRepository repository;public void enable(Long id){// 清理之前绑定的 HandlerDomainEventBusHolder.clean();// 注册 EventHandlerAccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);Optional<Account> accountOptional = repository.getById(id);if (accountOptional.isPresent()) {Account account = accountOptional.get();// enable 使用 DomainEventBusHolder 直接发布事件account.enable();repository.save(account);}}class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{@Overridepublic void handle(AccountEnabledEvent event) {LOGGER.info("handle enable event");}}
}
复制代码
2.2.3 基于实体缓存的事件发布

先将事件缓存在实体中,在实体状态成功持久化到存储后,再进行事件发布。

具体交互如下:

实例代码如下:

public class Account extends JpaAggregate {public void enable(){AccountEnabledEvent event = new AccountEnabledEvent(this);registerEvent(event);}
}
复制代码

Accountenable 方法,调用 registerEvent 对事件进行注册。

@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.Transientprivate 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@JsonIgnorepublic List<DomainEvent> getEvents() {return Collections.unmodifiableList(events.stream().map(eventSupplier -> eventSupplier.getEvent()).collect(Collectors.toList()));}@Overridepublic 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;}}
}复制代码

registerEvent 方法在 AbstractAggregate 中,registerEvent 方法将事件保存到 events 集合,getEvents 方法获取所有事件,cleanEvents 方法清理缓存的事件。

Application 实例如下:

@Service
public class AccountApplication extends AbstractApplication {private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);@Autowiredprivate AccountRepository repository;@Autowiredprivate DomainEventBus domainEventBus;@PostConstructpublic void init(){// 使用 Spring 生命周期注册事件处理器this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());}public void enable(Long id){Optional<Account> accountOptional = repository.getById(id);if (accountOptional.isPresent()) {Account account = accountOptional.get();// enable 将事件缓存在 account 中account.enable();repository.save(account);List<DomainEvent> events = account.getEvents();if (!CollectionUtils.isEmpty(events)){// 成功持久化后,对事件进行发布this.domainEventBus.publishAll(events);}}}class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{@Overridepublic void handle(AccountEnabledEvent event) {LOGGER.info("handle enable event");}}
}
复制代码

AccountApplicationinit 方法完成事件监听器的注册,enable 方法在实体成功持久化后,将缓存的事件通过 DomainEventBus 实例 publish 出去。

2.2.4 由调用方发布事件

通常情况下,领域事件是由聚合的命令方法产生,并在命令方法执行成功后,进行事件的发布。 有时,领域事件并不是聚合中的命令方法产生的,而是由用户所发生的请求产生。

此时,我们需要将领域事件建模成一个聚合,并且拥有自己的资源库。但,由于领域事件表示的是过去发生的事情,因此资源库只做追加操作,不能对事件进行修改和删除功能。

例如,对用户点击事件进行发布。

@Entity
@Data
public class ClickAction extends JpaAggregate implements DomainEvent {@Setter(AccessLevel.PRIVATE)private Long userId;@Setter(AccessLevel.PRIVATE)private String menuId;public ClickAction(Long userId, String menuId){Preconditions.checkArgument(userId != null);Preconditions.checkArgument(StringUtils.isNotEmpty(menuId));setUserId(userId);setMenuId(menuId);}@Overridepublic String id() {return String.valueOf(getId());}@Overridepublic Date occurredOn() {return getCreateTime();}}
复制代码

ClickAction 继承自 JpaAggregate 实现 DomainEvent 接口,并重写 id 和 occurredOn 方法。

@Service
public class ClickActionApplication extends AbstractApplication {@Autowiredprivate ClickActionRepository repository;@Autowiredprivate DomainEventBus domainEventBus;public void clickMenu(Long id, String menuId){ClickAction clickAction = new ClickAction(id, menuId);clickAction.prePersist();this.repository.save(clickAction);domainEventBus.publish(clickAction);}
}
复制代码

ClickActionApplication 在成功保存 ClickAction 后,使用 DomainEventBus 对事件进行发布。

2.3 订阅领域事件

由什么组件向领域事件注册订阅器呢?大多数请求,由应用服务完成,有时也可以由领域服务进行注册。

由于应用服务是领域模型的直接客户,它是注册领域事件订阅器的理想场所,即在应用服务调用领域方法之前,就完成了对事件的订阅。

基于 ThreadLocal 进行订阅:

public void enable(Long id){// 清理之前绑定的 HandlerDomainEventBusHolder.clean();// 注册 EventHandlerAccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);Optional<Account> accountOptional = repository.getById(id);if (accountOptional.isPresent()) {Account account = accountOptional.get();// enable 使用 DomainEventBusHolder 直接发布事件account.enable();repository.save(account);}
}
复制代码

基于实体缓存进行订阅:

@PostConstruct
public void init(){// 使用 Spring 生命周期注册事件处理器this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());
}public void enable(Long id){Optional<Account> accountOptional = repository.getById(id);if (accountOptional.isPresent()) {Account account = accountOptional.get();// enable 将事件缓存在 account 中account.enable();repository.save(account);List<DomainEvent> events = account.getEvents();if (!CollectionUtils.isEmpty(events)){// 成功持久化后,对事件进行发布this.domainEventBus.publishAll(events);}}
}
复制代码

2.4 处理领域事件

完成事件发布后,让我们一起看下事件处理。

2.4.1 保证聚合间的数据一致性

我们通常将领域事件用于维护模型的一致性。在聚合建模中有一个原则,就是在一个事务中,只能对一个聚合进行修改,由此产生的变化必须在独立的事务中运行。

在这种情况下,需要谨慎处理的事务的传播性。

应用服务控制着事务。不要在事件通知过程中修改另一个聚合实例,因为这样会破坏聚合的一大原则:在一个事务中,只能对一个聚合进行修改。

对于简单场景,我们可以使用特殊的事务隔离策略对聚合的修改进行隔离。具体流程如下:

但,最佳方案是使用异步处理。及每一个定义方都在各自独立的事务中修改额外的聚合实例。

事件订阅方不应该在另一个聚合上执行命令方法,因为这样将破坏“在单个事务中只修改单个聚合实例”的原则。所有聚合实例间的最终一致性必须通过异步方式处理。

详见,异步处理领域事件。

2.4.2 替换批量处理

批处理过程通常需要复杂的查询,并且需要庞大的事务支持。如果在接收到领域事件时,系统就立即处理,业务需求不仅得到了更快的满足,而且杜绝了批处理操作。

在系统的非高峰时期,通常使用批处理进行一些系统的维护,比如删除过期数据、创建新的对象、通知用户、更新统计信息等。这些批处理往往需要复杂的查询,并需要庞大的事务支持。

如果我们监听系统中的领域事件,在接收领域事件时,系统立即处理。这样,原本批量集中处理的过程就被分散成许多小的处理单元,业务需要也能更快的满足,用户可以可以及时的进行下一步操作。

2.4.3 实现事件源模式

对于单个限界上下文中的所有领域事件,为它们维护一个事件存储具有很多的好处。

对事件进行存储可以:

  • 将事件存储作为消息队列来使用,然后将领域事件通过消息设施发布出去。
  • 将事件存储用于基于 Rest 的事件通知。
  • 检查模型命名方法产生结果的历史记录。
  • 使用事件存储来进行业务预测和分析。
  • 使用事件来重建聚合实例。
  • 执行聚合的撤销操作。

事件存储是个比较大的课题,将有专门章节进行讲解。

2.4.4 进行限界上下文集成

基于领域事件的限界上下文集成,主要由消息队列和 REST 事件两种模式。

在此,重心讲解基于消息队列的上下文集成。

在不同的上下文中采用消息系统时,我们必须保证最终一致性。在这种情况下,我们至少需要在两种存储之间保存最终一致性:领域模型所使用的存储和消息队列所使用的持久化存储。我们必须保证在持久化领域模型时,对于的事件也已经成功发布。如果两种不同步,模型可能会处于不正确的状态。

一般情况下,有三种方式:

  1. 领域模型和消息共享持久化存储。在这种情况下,模型和事件的提交在一个事务中完成,从而保证两种的一致性。
  2. 领域模型和消息由全局事务控制。这种情况下,模型和消息所用的持久化存储可以分离,但会降低系统性能。
  3. 在领域持久化存储中,创建一个特殊的存储区域用于存储事件(也就是事件存储),从而在本地事务中完成领域和事件的存储。然后,通过后台服务将事件异步发送到消息队列中。

一般情况下,第三种,是比较优雅的解决方案。

在一致性要求不高时,可以通过领域事件订阅器直接向消息队列发送事件。具体流程如下:

对一致性要求高时,需要先将事件存储,然后通过后台线程加载并分发到消息队列。具体流程如下:

2.5 异步处理领域事件

领域事件可以与异步工作流程协同,包括限界上下文间使用消息队列进行异步通信。当然,在同一个限界上下文中,也可以启动异步处理流程。

作为事件的发布者,不应关心是否执行异步处理。异常处理是由事件执行者决定。

DomainEventExecutor 提供对异步处理的支持。

DomainEventExecutor eventExecutor  =new ExecutorBasedDomainEventExecutor("EventHandler", 1, 100);
this.domainEventBus.register(AccountEnabledEvent.class,eventExecutor,new AccountEnableEventHandler());
复制代码

异步处理,就意味着放弃数据库事务的 ACID 特性,而选择使用最终一致性。

2.6 内部事件与外部事件

使用领域事件时需要对事件进行区分,以避免技术实现的问题。

认识内部事件和外部事件之间的区别至关重要。

  • 内部事件,是一个领域模型内部的事件,不在有界上下文间进行共享。
  • 外部事件,是对外发布的事件,在多个有界上下文中进行共享。

一般情况下,在典型的业务用例中,可能会有很多的内部事件,而只有一两个外部事件。

2.6.1 内部事件

内部事件存在于限界上下文内部,受限界上下文边界保护。

内部事件被限制在单个有界上下文边界内部,所以可以直接引用领域对象。

public interface AggregateEvent<ID, A extends Aggregate<ID>> extends DomainEvent{A source();default A getSource(){return source();}
}
复制代码

比如 AggregateEvent 中的 source 指向发布该事件的聚合。

public class LikeSubmittedEvent extends AbstractAggregateEvent<Long, Like> {public LikeSubmittedEvent(Like source) {super(source);}public LikeSubmittedEvent(String id, Like source) {super(id, source);}
}
复制代码

LikeSubmittedEvent 类直接引用 Like 聚合。

2.6.2 外部事件

外部事件存在于限界上下文间,被多个上下文共享。

一般情况下,外部事件,只作为数据载体存在。常常采用平面结构,并公开所有属性。

@Data
public class SubmittedEvent {private Owner owner;private Target target;
}
复制代码

SubmittedEvent 为扁平化结构,主要是对数据的封装。

由于外部事件被多个上下文共享,版本管理就显得非常重要,以避免重大更改对其服务造成影响。

3 实现领域事件模式

领域事件是一种通用模式,它的本质是将领域概念添加到发布-订阅模式。

3.1 封装领域事件的“发布-订阅”模式

发布-订阅是比较成熟的设计模式,具有很高的通用性。因此,建议针对领域需求进行封装。

比如直接使用 geekhalo-ddd 相关模块。

定义领域事件:

@Value
public class LikeCancelledEvent extends AbstractAggregateEvent<Long, Like> {public LikeCancelledEvent(Like source) {super(source);}
}
复制代码

订阅领域事件:

this.domainEventBus.register(LikeCancelledEvent.class, likeCancelledEvent->{CanceledEvent canceledEvent = new CanceledEvent();canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());this.redisBasedQueue.pushLikeEvent(canceledEvent);});
复制代码

异步执行领域事件:

DomainEventExecutor eventExecutor  =new ExecutorBasedDomainEventExecutor("LikeEventHandler", 1, 100);
this.domainEventBus.register(LikeCancelledEvent.class, eventExecutor, likeCancelledEvent->{CanceledEvent canceledEvent = new CanceledEvent();canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());this.redisBasedQueue.pushLikeEvent(canceledEvent);});
复制代码

3.2 内存总线处理内部事件,消息队列处理外部事件

内存总线简单高效,同时支持同步、异步两个处理方案,比较适合处理繁杂的内部事件;消息队列虽然复杂,但擅长解决服务间通信问题,适合处理外部事件。

3.3 使用实体缓存领域事件

理论上,只有在业务成功完成后,才应该对外发布事件。因此,将领域事件缓存在实体中,并在完成业务操作后将其进行发布,是一种较好的解决方案。

相比,使用 ThreadLocal 管理订阅器,并在事件 publish 时进行订阅回调,事件缓存方案有明显的优势。

3.4 使用 IOC 容器的事件发布功能

IOC 容器为我们提供了很多使用功能,其中也包括发布-订阅功能,如 Spring。

通常情况下,领域模型不应该直接依赖于 Spring 容器。因此,在领域中我们仍然使用内存总线,为其添加一个订阅者,将内存总线中的事件转发到 Spring 容器中。

class SpringEventDispatcher implements ApplicationEventPublisherAware {@Autowiredprivate DomainEventBus domainEventBus;private ApplicationEventPublisher eventPublisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.eventPublisher = applicationEventPublisher;}@PostConstructpublic void addListener(){this.domainEventBus.register(event->true, event -> {this.eventPublisher.publishEvent(event);});}}
复制代码

此时,我们就可以直接使用 Spring 的 EventListener 机制对领域事件进行处理。

@Component
public class RedisBasedQueueExporter {@Autowiredprivate RedisBasedQueue redisBasedQueue;@EventListenerpublic void handle(LikeSubmittedEvent likeSubmittedEvent){SubmittedEvent submittedEvent = new SubmittedEvent();submittedEvent.setOwner(likeSubmittedEvent.getSource().getOwner());submittedEvent.setTarget(likeSubmittedEvent.getSource().getTarget());this.redisBasedQueue.pushLikeEvent(submittedEvent);}@EventListenerpublic void handle(LikeCancelledEvent likeCancelledEvent){CanceledEvent canceledEvent = new CanceledEvent();canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());this.redisBasedQueue.pushLikeEvent(canceledEvent);}}
复制代码

4 小结

  • 领域事件是发生在问题域中的事实,它是通用语言的一部分。
  • 领域事件优先使用发布-订阅模式,会发布事件并且触发相应的事件处理器。
  • 限界上下文内,优先使用内部事件和内存总线;限界上下文间,优先使用外部事件和消息队列。
  • 领域事件使异步操作变得简单。
  • 领域事件为聚合间提供了最终一致性。
  • 领域事件可以将大的批量操作简化为许多小的业务操作。
  • 领域事件可以完成强大的事件存储。
  • 领域事件可以完成限界上下文间的集成。
  • 领域事件是更复杂架构(CQRS)的一种支持。

领域驱动设计战术模式--领域事件相关推荐

  1. 领域驱动设计战术模式:领域事件

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第五篇,重心讲解领域事件模式. 在建模时,有时会遇 ...

  2. 领域驱动设计战术模式:领域服务

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第四篇,重心讲解领域服务模式. 在建模时,有时会遇 ...

  3. 领域驱动设计战术模式:实体

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第三篇,重心讲解实体模式. 实体是具有唯一标识的概 ...

  4. 领域驱动设计战术模式--值对象

    值对象虽然经常被掩盖在实体的阴影之下,但它却是非常重要的 DDD 概念. 值对象不具有身份,它纯粹用于描述实体的特性.处理不具有身份的值对象是很容易的,尤其是不变性与可组合性是支持易用性的两个特征. ...

  5. 我的领域驱动设计运用实例 - 领域啊领域

    一.前言 断断续续的也有在闲余时间接触领域驱动设计的相关知识,因为目前在工作中更多的还只是一名 crud boy,因此目前也只是对其中的某些知识点有知晓,实际使用的比较少,仅此而已.因此,趁着这个春节 ...

  6. 领域驱动设计-什么是领域驱动设计和怎么使用它

    这篇文章讨论领域驱动设计(DDD),DDD是建立在面向对象分析设计上开发软件的一种方法. 通过这篇文章我们解释什么是领域驱动设计,在现代开发周期中如何实现,使用DDD的优点和缺点. 什么是领域 定义D ...

  7. DDD领域驱动设计三、用事件风暴构建领域模型

    文章目录 一.准备事件风暴 1.参与人员 2.环境条件 二.确定产品愿景 参与角色: 三.业务场景分析 1.参与角色: 2.实例 四.领域建模 1.参与角色: 2.思考 3.实例 五.微服务拆分与设计 ...

  8. 如何运用领域驱动设计 - 领域服务

    概述 本文将介绍领域驱动设计(DDD)战术模式中另一个非常重要的概念 - 领域服务.在前面两篇博文中,我们已经学习到了什么是值对象和实体,并且能够比较清晰的定位它们自身的行为.但是在某些时候,你会发现 ...

  9. 如何运用领域驱动设计 - 聚合

    概述 DDD实战与进阶 - 值对象 如何运用DDD - 实体 如何运用领域驱动设计 - 领域服务 在前几篇的博文中,我们已经学习到了如何运用实体和值对象.随着我们所在领域的不断深入,领域模型变得逐渐清 ...

最新文章

  1. python写游戏脚本-python实现简单贪吃蛇游戏
  2. 模块的概念,组建地概念?
  3. 计算机硬件部分可称为裸机,上财信管PPT第3章 计算机硬件与软件基础.ppt
  4. java radiobutton获取信息_如何获取JRadioButton的文本值
  5. python3小游戏源代码_Python3制作仿“FlappyBird”小游戏|python3教程|python入门|python教程...
  6. Java IO API记录
  7. 校办研修之计算机培训简报,“2018校本研修培训”第二期学习简报
  8. 2013=726 整合,优化,利用自身资源。 让自己的时间更有意义,最大化利用
  9. 在龙芯电脑制作 debian 系统(MIPS)
  10. 计算机网络——SMTP
  11. 首家新三板上市区块链公司:他们提供面向政务、商务、公众等解决方案
  12. 2,词根 - 抓、拿
  13. 关于python3.9安装Airtest跑脚本“jinja2”报错的问题说明
  14. 7月第1周风控关注 微信支付SDK曝XXE漏洞 可伪造订单
  15. vscode 逗号不换行_苹果手机九宫格怎么换行 苹果手机九宫格换行操作步骤
  16. 2022最新SpringCloud Alibaba入门到精通超详细版文档,教你从0到1搭建一个微服务项目
  17. xshell连接不上虚拟机问题,前提:虚拟机和主机能ping通
  18. 深入理解JVM虚拟机之垃圾回收
  19. 刚颁布的《中国贫富标准线》,
  20. ARP原理和ARP攻击

热门文章

  1. CentOS6.x 下 LNMP环境搭建(二、安装 Nginx)
  2. MySQL错误:The user specified as a definer (XXX@XXX) does not exist
  3. 在IE中用js改变table的innerHTML属性报“未知的运行时错误”
  4. JRuby 1.6.0发布
  5. Elasticsearch 嵌套类型nested
  6. TCP慢开始与拥塞避免
  7. HDU 1285:确定比赛名次(拓扑排序)
  8. 2017年山东省两化融合深度行临沂站成功举办
  9. Oracle把Java EE的未来押在Rest API上了?
  10. 手机日期插件jquery mobiscroll 实例