在問題空間中存在不少具備固有身份的概念,一般狀況下,這些概念將建模爲實體。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;
}
複製代碼