精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

新項目從零到一DDD實戰(zhàn)思考與總結

開發(fā) 架構
領域驅動設計(DDD)是一種業(yè)務領域建模方法論、業(yè)務架構設計方法論,戰(zhàn)略設計階段從業(yè)務領域視角劃分領域邊界,抽象業(yè)務建立領域模型;戰(zhàn)術設計階段則根據(jù)清晰的領域邊界、領域模型進行架構設計與開發(fā)實現(xiàn)。

[[408250]]

領域驅動設計(DDD)是一種業(yè)務領域建模方法論、業(yè)務架構設計方法論,戰(zhàn)略設計階段從業(yè)務領域視角劃分領域邊界,抽象業(yè)務建立領域模型;戰(zhàn)術設計階段則根據(jù)清晰的領域邊界、領域模型進行架構設計與開發(fā)實現(xiàn)。

DDD解決了核心復雜業(yè)務設計問題,簡化業(yè)務系統(tǒng)的實現(xiàn),讓業(yè)務邏輯高度內聚,與基礎設施、框架解耦,清晰的領域邊界解決微服務的拆分問題。聚合之間通過聚合根ID引用與領域事件松耦合,保持高內聚、松耦合讓項目代碼隨著業(yè)務需求的不斷迭代保持整潔。

本篇筆者以近期的一個項目實戰(zhàn)跟大家分享筆者目前對DDD的理解,以及在實戰(zhàn)DDD過程中遇到的問題思考與總結,僅個人經(jīng)驗,偏戰(zhàn)術設計。

從實戰(zhàn)項目中理解DDD核心概念

領域通常指的就是業(yè)務范圍,每個公司都有自己明確的業(yè)務范圍。通常每個公司內部都有很多個系統(tǒng),如一家電商公司可能會有物流系統(tǒng)、電商系統(tǒng)、直播系統(tǒng)等等,每個系統(tǒng)做的事情則是更細分的領域。

茉莉紅交所(紅探長)項目是筆者入職茉莉數(shù)科集團后做的第一個項目,也是一個新的項目,由于沒有歷史包袱,筆者從零開始搭建整個項目,因此選擇在該項目試行DDD。

該項目業(yè)務是OTO(線上到線下)探店,OTO探店就是該項目的領域。

探店其實是一種商家線上付費下單、平臺為商家匹配達人、達人線下探店線上內容推廣的內容營銷模式,可以是品嘗美食或是免門票游玩景點等,達人最終通過短視頻、直播、圖文內容等方式為商家做推廣。那么無論是探美食店、探游樂園,探店都是這個領域的核心。

在探店這個領域中,核心的業(yè)務名詞有:商家、達人、店鋪、訂單、任務,而核心事件有:店鋪入駐、商家發(fā)布訂單、達人接單等。

附上此項目對應版本的架構設計圖:

 提示:中間部分不是每個限界上下文對應一個微服務,可以是多個限界上下文合并在一個微服務。

限界上下文是業(yè)務概念的邊界,是業(yè)務問題最小粒度的劃分。在OTO探店業(yè)務領域中會包含多個限界上下文,我們通過找出這些確定的限界上下文對系統(tǒng)進行解耦,要求每一個限界上下文其內部必須是緊密組織的、職責明確的、具有較高的內聚性。

我們劃分出的限界上下文如下圖所示。

提示:為什么將任務和訂單拆分為不同限界上下文(任務不是作為訂單聚合根的實體,而是作為一個獨立聚合的聚合根)?這是因為商家發(fā)布的一個訂單允許有不同的多個達人接單,一個達人也可以接不同商家的訂單,這并不是簡單的一對多關系。這更像是商品與訂單的關系,而不是訂單與訂單item的關系。

在劃分出限界上下文后,還需要根據(jù)限界上下文識別出問題子域。問題子域是對業(yè)務問題的劃分,相對限界上下文來說,是對業(yè)務問題更大粒度的劃分。

  • 核心(子)域:產(chǎn)品的核心競爭力、盈利來源;
  • 通用子域:常見的,不同領域都可共用的,可通過購買就能使用的;
  • 支撐子域:非核心域、又非通用域,具有個性化需求,用于支撐核心域運作;

根據(jù)限界上下文,我們劃分出的子域如下圖所示。

  • OTO探店核心域:商家下單、平臺審核訂單、達人接單、平臺審核任務、達人回填內容鏈接等;
  • 店鋪支撐子域:商家店鋪入駐、平臺審核店鋪、商家綁定店鋪、店鋪轉移等;
  • ......

在劃分出子域后,我們需要為領域建模。

領域建模是通過將業(yè)務抽象為聚合、實體、聚合根、值對象模型的方式,封裝和承載全部的業(yè)務邏輯,保持業(yè)務的高內聚和低耦合。

聚合:負責封裝業(yè)務邏輯,內聚決策命令和領域事件,容納實體、聚合根、值對象。

  • 聚合根:也是一種實體,是聚合的根節(jié)點,如訂單;
  • 實體:聚合的主干,具有唯一標識和生命周期,如訂單Item;
  • 值對象:實體的附加業(yè)務概念,用于描述實體所包含的業(yè)務信息,如訂單收件地址。

70%的場景下,一個聚合內都只有一個實體,那就是聚合根。

在技術實現(xiàn)上,一個聚合就是一個包,里面存放領域服務、工廠、資源庫、聚合根、實體、值對象。

領域層包的劃分規(guī)則通常為:

  1. ----限界上下文 
  2. --domain 
  3. ------聚合A 
  4. -------- (聚合根、實體、值對象、領域服務、資源庫、領域事件) 
  5. ------聚合B 
  6. -------- (聚合根、實體、值對象、領域服務、資源庫、領域事件) 

特別的,一個限界上下文可能包含多個聚合,但一個聚合只能存在于一個限界上下文。

以上包的劃分只是領域層的劃分,要求聚合根、實體、值對象、領域服務、資源庫、領域事件等類存放在聚合包下,無論是使用DDD經(jīng)典四層架構,還是六邊形架構。

以店鋪上下文為例,我們采樣六邊形架構實現(xiàn),整個模塊的層次、包劃分如下:

  1. com.mmg.hjs.storecontext(限界上下文) 
  2. --adapters(適配層) 
  3. ----api(API接口適配) 
  4. ------webmvc(前端接口,請求從網(wǎng)關進來) 
  5. ------dubbo(內部微服務調用,實現(xiàn)應用層gateway包下的接口) 
  6. ----persistence(持久化適配) 
  7. ------dao(mybatis的Mapper類與xml文件) 
  8. ------po(對應數(shù)據(jù)庫表生成的類) 
  9. ------StoreAssembler.class(領域實體轉PO的轉換器) 
  10. ------StoreRepositoryImpl.class(實現(xiàn)領域層的資源庫接口) 
  11. ----cache(緩存適配) 
  12. --application(應用層) 
  13. ----gateway(與其它限界上下文通信并與適配層接耦的抽象接口) 
  14. ----job(可選,定時任務) 
  15. ----usecase(應用服務,按用例拆分多個類,避免單個類臃腫) 
  16. ----assembler(聚合根轉DTO轉換器) 
  17. ----dto(DTO類) 
  18. ----cqe(CQE模式) 
  19. ------command(請求參數(shù)) 
  20. ------query(查詢參數(shù)) 
  21. ------event(可選,事件參數(shù),注意:非領域事件) 
  22. --domain(領域層) 
  23. ----store(店鋪聚合) 
  24. ------model(聚合根、實體、值對象) 
  25. ------event(領域事件) 
  26. ------StoreDomainService.class(領域服務) 
  27. ------StoreRepository.class(資源庫抽象接口) 

在分層架構模式中,我們需要嚴格遵循:上層只能依賴下層,下層不能依賴上層。

例如,在一次創(chuàng)建訂單的操作中,用于接收前端請求參數(shù)的CreateOrderCommand屬于應用層的類,雖然我們在Controller(接口層)可直接使用CreateOrderCommand,但這屬于上層依賴下層,并且不是領域層,也并未暴露聚合根內部結構,因此是允許的。

如果反過來,直接將CreateOrderCommand對象傳遞給聚合根,那就構成下層依賴上層了,因此這是不允許的。CreateOrderCommand必須在應用層拆解為創(chuàng)建訂單所需要的值對象,或者實體對象,再調用領域服務方法,領域服務方法調用訂單工廠創(chuàng)建訂單,最后才交給資源庫持久化訂單聚合根。

在領域層里面,更準確的說是在聚合包內,存儲的是聚合下的聚合根、實體、值對象、資源庫、領域服務、聚合根工廠類,而由于資源庫的實現(xiàn)需要依賴orm框架或其它框架實現(xiàn)持久化聚合根、事件的發(fā)布需要依賴MQ實現(xiàn)等,所以資源庫定義成接口由上層實現(xiàn),事件發(fā)布也定義為接口由上層實現(xiàn)。

對于資源庫與事件發(fā)布者,由于領域服務需要依賴資源庫獲取和保存聚合根,也依賴事件發(fā)布者發(fā)布事件,這種情況下我們可以使用Spring框架的自動注入,但應該使用構造函數(shù)注入,而不是在字段上加注解的方式注入,避免注入資源庫、事件發(fā)布者為NULL的情況,且不應該添加Spring框架的注解(盡量不耦合)。

應用服務、領域服務、聚合根、資源庫的職責

在實現(xiàn)DDD的過程中,我們需要嚴格遵守代碼規(guī)范才能保持代碼的整潔,否則隨著需求的迭代,項目很容易就失去DDD該有的模樣,變得即不DDD也不MVC。

在DDD中,Repository(資源庫)是聚合根的容器,與DAO扮演相同角色,但它只提供持久化聚合根的操作(新增或更新),以及提供根據(jù)ID獲取聚合根的查詢操作。在所有的領域對象中,只有聚合根才擁有Repository,因為Repository不同于DAO,它所扮演的角色只是向領域模型提供聚合根。

資源庫(Repository) 的職責是提供聚合根或者持久化聚合根,除此之外應「盡可能」的沒有其它行為,否則聚合根就會嚴重退化成DAO。

  1. public interface Repository<DO, KEY> { 
  2.     void save(DO obj); 
  3.     DO findById(KEY id); 
  4.     void deleteById(KEY id); 

聚合根與領域服務(DomainService) 負則封裝實現(xiàn)業(yè)務邏輯,應用服務(ApplicationService) 不處理業(yè)務邏輯,而只是對領域服務/聚合根方法調用的封裝。

正常情況下,處理一次業(yè)務請求將經(jīng)過:

  • 應用服務->領域服務->通過資源庫獲取聚合根
  • ->通過資源庫持久化聚合根
  • ->發(fā)布領域事件

但也允許:

  • 應用服務->通過資源庫獲取聚合根
  • ->通過資源庫持久化聚合根
  • ->發(fā)布領域事件

對于不能直接通過聚合根完成的業(yè)務操作就需要通過領域服務。

但必須遵守原則:

  • 聚合根不能直接操作其它聚合根,聚合根與聚合根之間只能通過聚合根ID引用;
  • 同限界上下文內的聚合之間的領域服務可直接調用;
  • 兩個限界上下文的交互必須通過應用服務層抽離接口->適配層適配。

聚合根工廠負責創(chuàng)建聚合根,但并非必須的,可以將聚合根的創(chuàng)建寫到聚合根下并改為靜態(tài)方法,非常復雜的創(chuàng)建過程才建議寫工廠類。

以修改用戶信息為例,可在應用服務通過資源庫獲取用戶聚合根,再調用用戶聚合根的修改用戶信息方法,最后通過資源庫持久化用戶聚合根。

  1. public class UserModifyInfoUseCase{ 
  2.  
  3.     /** 
  4.      * 更新用戶基本信息 
  5.      * 
  6.      * @param command 
  7.      * @param token 
  8.      */ 
  9.     @Transactional(rollbackFor = Throwable.class, isolation = Isolation.READ_COMMITTED) 
  10.     public void updateUserInfo(ModifyUserInfoCommand command, Long loginUserId) { 
  11.         // 獲取聚合根 
  12.         Account account = findByAccountId(loginUserId); 
  13.         // 調用業(yè)務方法 
  14.         account.modifyAccountInfo(AccountInfoValobj.builder() 
  15.                 .nickname(command.getNickname()) 
  16.                 .avatarUrl(command.getAvatarUrl()) 
  17.                 .country(command.getCountry()) 
  18.                 .province(command.getProvince()) 
  19.                 .city(command.getCity()) 
  20.                 .gender(Sex.valueBy(command.getGender())) 
  21.                 .build()); 
  22.         // 通過資源庫持久化 
  23.         repository.save(account); 
  24.         // 更新緩存 
  25.         accountCache.cache(loginUserId, getUserById(account.getId())); 
  26.     } 
  27.  

這里用戶聚合根能到看到自己的信息,用戶自己修改自己的信息可直接通過聚合根完成,因此這種場景下我們不需要領域服務。

復雜場景如用戶綁定手機號碼就不能直接在領域服務中完成。

綁定手機號碼一般流程為:獲取短信驗證碼、校驗短信驗證碼、校驗手機號碼是否已經(jīng)綁定了別的賬號。

其中獲取短信驗證碼與校驗短信驗證碼應放在應用服務完成,而校驗手機號碼是否已經(jīng)綁定了別的賬號就需要由領域服務完成,因為聚合根無法完成這個判斷, 聚合根看不到別的賬號,聚合根不能擁有資源庫,且應用服務不能處理業(yè)務邏輯。

聚合根

  1. public class Account extends BaseAggregate<AccountEvent>{ 
  2.     // ..... 
  3.     private String phone; 
  4.  
  5.     public void bindMobilePhone(String phoneNumber) { 
  6.         if (!StringUtils.isEmpty(this.phone)) { 
  7.             throw new AccountParamException("已經(jīng)綁定過手機號碼了,如需更新可走更換手機號碼流程"); 
  8.         } 
  9.         this.phone = phoneNumber; 
  10.     } 
  11.  

領域服務

  1. @Service 
  2. public class AccountDomainService { 
  3.      
  4.     private AccountRepository repository; 
  5.  
  6.     public AccountDomainService(AccountRepository repository) { 
  7.         this.repository = repository; 
  8.     } 
  9.  
  10.     public void bindMobilePhone(Long userId, String phone) { 
  11.         Account account = repository.findById(userId); 
  12.         if (account == null) { 
  13.             throw new AccountNotFoundException(userId); 
  14.         } 
  15.         // 號碼被其它賬戶綁定了 
  16.         boolean exist = repository.findByPhone(phone) != null
  17.         if (exist) { 
  18.             throw new AccountBindPhoneException(phone); 
  19.         } 
  20.         account.bindMobilePhone(phone); 
  21.         repository.save(account); 
  22.     } 
  23.  

應用服務

  1. @Service 
  2. public class UserBindPhoneUseCase { 
  3.      
  4.        /** 
  5.          * 綁定手機號碼-發(fā)送驗證碼 
  6.          * 
  7.          * @param command 
  8.          * @param token 
  9.          */ 
  10.         public void bindMobilePhoneSendVerifyCode(VerifyCodeSendCommand command, Long loginUserId) { 
  11.             // 生成驗證碼 
  12.             String verifyCode = ValidCodeUtils.generateNumberValidCode(4); 
  13.             // 緩存驗證碼 
  14.             verifyCodeCache.save(command.getPhone(),verifyCode,timeout); 
  15.             // 調用消息服務發(fā)送驗證碼 
  16.             messageClientGateway.sendSmsVerifyCode(command.getPhone(), verifyCode); 
  17.         } 
  18.      
  19.         /** 
  20.          * 綁定手機號碼-提交綁定 
  21.          * 
  22.          * @param command 
  23.          * @param token 
  24.          */ 
  25.         public void bindMobilePhone(BindPhoneCommand command, Long userId) { 
  26.             // 校驗驗證碼 
  27.             String verifyCode = verifyCodeCache.get(command.getPhone()); 
  28.             if (!command.getVerifyCode().equalsIgnoreCase(verifyCode)) { 
  29.                 throw new VerifyPhoneCodeApplicationException(); 
  30.             } 
  31.             // 通過領域服務綁定手機號碼 
  32.             accountDomainService.bindMobilePhone(userId, command.getPhone()); 
  33.             // 更新賬號緩存 
  34.             accountCache.cache(userId, getUserById(userId)); 
  35.         } 

接口層

  1. @RestController 
  2. @RequestMapping("account/bindMobilePhone"
  3. public class UserBindPhoneController { 
  4.   
  5.     @Resource 
  6.     private UserBindPhoneUseCase useCase; 
  7.  
  8.     @ApiOperation("綁定手機號-獲取驗證碼"
  9.     @GetMapping("/verifyCode"
  10.     public Response<Void> bindMobilePhone(@RequestParam("phone") String phone) { 
  11.         Long userId = WebUtils.getLoginUserId(); 
  12.         useCase.bindMobilePhoneSendVerifyCode(phone, userId); 
  13.         return Response.success(); 
  14.     } 
  15.  
  16.     @ApiOperation("綁定手機號-提交綁定"
  17.     @PostMapping("/submit"
  18.     public Response<Void> bindMobilePhone(@RequestBody @Validated BindPhoneCommand command) { 
  19.         Long userId = WebUtils.getLoginUserId(); 
  20.         useCase.bindMobilePhone(command, userId); 
  21.         return Response.success(); 
  22.     } 
  23.      

CQE模式

CQE即Command、Query、Event。接收前端創(chuàng)建訂單請求使用Command,接收前端分頁查詢請求使用Query,消費事件(非領域事件)則使用Event。

除Event外,所有寫請求都應該使用Command接收參數(shù),而所有查詢都應該使用Query接收參數(shù),只在參數(shù)只有一個ID的查詢情況下,可省略Query。

在查詢分離情況下,Query是可直接傳遞到DAO的(接口層->應用層->DAO)。因此使用Query封裝查詢條件能夠提高方法的復用,當添加查詢條件時,無需給方法加多一個參數(shù)。

CQRS模式

CQRS(Command Query Resposibility Segregation),即命令查詢職責分離模式。軟件模型中存在讀模型和寫模型之分,以我們寫業(yè)務代碼的經(jīng)驗也知道,一次請求,要么是作為一個“命令”執(zhí)行一次操作,要么作為一個”查詢“向調用方返回數(shù)據(jù),兩者不可能共存。CQRS是將“命令”和“查詢”分別使用不同的對象模型來表示。

CQRS的讀操作放在應用層。

共享存儲-共享模型-CQRS

共享存儲指同一個表結構存儲數(shù)據(jù),共享模型指使用聚合根從數(shù)據(jù)庫讀取數(shù)據(jù)。

例如查詢訂單詳情,訂單的聚合根為Order。

  1. // 訂單聚合根 
  2. public class Order extends BaseAggregate { 

在應用層OrderDetailsUseCase通過OrderRepository查詢訂單聚合根,再調用裝配器將聚合根轉為讀模型。

  1. public class OrderDetailsUseCase { 
  2.     public OrderDto byId(String id) { 
  3.         Order order = orderRepository.byId(id); 
  4.         return orderDaoAssembler.toDto(order); 
  5.     } 
  • OrderDaoAssembler方法是將Order轉為讀模型實體,也就是將DO轉為DTO。

注意:讀操作和寫操作不要寫在同一個應用服務中,避免耦合,且應用服務應按用例拆分多個類(不需要很細),避免應用服務越寫越臃腫。

共享存儲-讀寫分離模型-CQRS

共享存儲-讀寫分離模型指讀寫還是操作同一張表,只是寫模型與讀模型不同,寫通過聚合根操作,而讀模型繞過聚合根、Repository,直接操作數(shù)據(jù)庫,此時的讀模型就是用于裝載從數(shù)據(jù)庫查詢的數(shù)據(jù),并且不需要再作轉換就可以響應給調用方,這里的讀模型就是DTO。

對于單個聚合根內的查詢,使用「共享存儲-讀寫分離模型」模型可以應付復雜的查詢場景,并且可以提升性能。

對于需要跨多個聚合根的查詢,「共享存儲-共享模型」無法實現(xiàn)此需求場景,而分別查詢多個聚合根后,再合并查詢結果不僅是將原本簡單的事情變復雜,還大大影響性能,因此更有必要采用「共享存儲-讀寫分離模型」。

對于查詢訂單詳情希望帶上商品信息,如果商品與訂單在同一個服務,并且同一個數(shù)據(jù)庫,那么便可以使用join多表查詢。

共享存儲-讀寫分離模型-CQRS實戰(zhàn)舉例:

接口層

  1. @RequestMapping("/order"
  2. @RestController 
  3. public class OrderQueryController { 
  4.      
  5.     @GetMapping("/query"
  6.     public Response<PageInfo<OrderQueryDto>> queryOrder(OrderQuery query) { 
  7.         return Response.success(orderQueryUseCase.queryOrder(query,WebUtils.getLoginUserId())); 
  8.     } 
  9.  

應用層

  1. @Service 
  2. public class OrderQueryUseCase implements Cqrs { 
  3.  
  4.     public PageInfo<OrderQueryDto> queryOrder(OrderQuery query, Long loginUserId) { 
  5.         Long merchantId = merchantGateway.getMerchantId(loginUserId); 
  6.         IPage<OrderQueryDto> orderPage = new Page<>(query.getPage(), query.getPageSize()); 
  7.         List<OrderQueryDto> orders = orderMapper.selectOrderBy(merchantId,query,orderPage); 
  8.         PageInfo<OrderQueryDto> pageInfo = new PageInfo<>(page, pageSize); 
  9.         pageInfo.setTotalCount((int) orderPage.getTotal()); 
  10.         pageInfo.setList(orders); 
  11.         return pageInfo; 
  12.     } 

讀寫分離存儲-讀寫分離模型

即讀與寫操作不同數(shù)據(jù)庫。例如,對于查詢訂單詳情希望帶上商品信息,如果商品是一個微服務、訂單是一個微服務,并且兩個微服務使用不同的數(shù)據(jù)庫,如果要提升性能,就需要通過額外的數(shù)據(jù)同步服務,將訂單與商品查詢結果合并后存入一個新的表(分離存儲)或者是存儲到NoSQL數(shù)據(jù)庫。數(shù)據(jù)同步可通過底層數(shù)據(jù)庫Binlog+Kafka消費實現(xiàn),還有一種是通過消費領域事件實現(xiàn),但影響應用性能。

提示:對于復雜的報表統(tǒng)計,建議通過Binlog+Kafka同步到一張大表,為不與業(yè)務耦合,應獨立為一個數(shù)據(jù)服務。

領域事件的發(fā)布

在DDD中有一個原則,一個業(yè)務用例對應一個事務,一個事務對應一個聚合根,即在一次事務中只能對一個聚合根操作。

但在實際應用中,一個業(yè)務用例往往需要修改多個聚合根,而不同的聚合根可能在不同的限界上下文中,引入領域事件即不破壞DDD的一個事務只修改一個聚合根的原則,也能實現(xiàn)限界上下文之間的解耦。

在DDD中,領域層是業(yè)務邏輯的具體實現(xiàn),所有以解決問題子域的業(yè)務代碼都高度內聚在限界上下文中、高度內聚在聚合中,即聚合根、實體以及領域服務內。

對于領域事件發(fā)布,我們的實現(xiàn)是在聚合根中臨時保存,最后在領域服務/應用服務發(fā)布,領域層抽象事件發(fā)布接口,由適配器層實現(xiàn),并注入到領域服務/應用服務。

一個原因是領域層不應該依賴其它框架的Api,另一個原因則與領域事件是由聚合根/領域服務創(chuàng)建有關。

那為什么領域事件由聚合根/領域服務創(chuàng)建,而不是在應用層創(chuàng)建?

發(fā)布領域事件當然是在領域層發(fā)出,可以是聚合根發(fā)出,也可以在領域服務發(fā)出,業(yè)務在聚合下高度內聚,什么時候該發(fā)出什么事件也只有聚合內最清楚,應用服務不過是封裝業(yè)務實現(xiàn)的步驟。

由于業(yè)務邏輯最核心的實現(xiàn)是聚合根內,而聚合根是一個實體,不能說每次構造聚合根都傳入一個事件發(fā)布者,那從資源庫獲取聚合根時又由誰傳入事件發(fā)布者?

所以推薦的做法是在聚合根下臨時存儲聚合根發(fā)出的事件,在領域服務中、在調用資源庫持久化聚合根之后再發(fā)布領域事件。當然,在不使用領域服務的情況下,則由應用層在調用資源庫持久化聚合根之后再發(fā)布領域事件。

應用服務一個業(yè)務用例只是對應領域服務方法的一層很薄的封裝,即不會在一個應用服務方法中調用兩個領域服務方法。這實際上也是我們要注意,如果出現(xiàn)這種情況,說明領域服務方法封裝的不夠好。所以領域事件由領域服務發(fā)布是允許的,但事件發(fā)布者必須抽象為接口,與資源庫一樣在領域服務的構建方法傳入。

關于我們?yōu)槭裁聪韧ㄟ^Spring框架發(fā)布事件再在訂閱者中實現(xiàn)將事件發(fā)布到MQ。

一個事件可能當前限界上下文內也需要消費,即可能有多個限界上下文需要消費,一個事件對應多個消費者。

如訂單限界上下文內訂單聚合產(chǎn)生的創(chuàng)建訂單事件,訂單限界上下文內需要消費訂單創(chuàng)建事件,用于構造消息通知,然后給消息通知隊列寫入消息;同時,其它限界上下文也需要消費訂單創(chuàng)建事件。因此,一個領域事件我們可能需要發(fā)布到多個消息隊列中。

先通過Spring框架發(fā)布事件再在訂閱者中實現(xiàn)將事件發(fā)布到MQ其實是借助Spring框架實現(xiàn)責任鏈模式。這當然不是必須的,也算不上是規(guī)范。

但不管如何,不要直接在領域服務/應用服務中調用MQ的API直接發(fā)布事件,發(fā)布事件到MQ應在適配層實現(xiàn)。并且封裝事件發(fā)布者的好處在于,當需要確保消息至少投遞一次時,這些邏輯不需要寫在應用服務中。

最令人頭疼的代碼

在實戰(zhàn)DDD的過程中,我們編寫最多的代碼無疑就是DO(聚合根)轉DTO(讀模型)以及DO轉PO(映射到數(shù)據(jù)庫表)和PO轉DO的轉換器代碼。百分之八十的BUG都來自這些整齊劃一的屬性拷貝代碼,容易漏字段。

那為什么需要這么多層轉換呢,直接將聚合根響應給請求、直接持久化聚合根不行嗎?

首先,在DDD中我們必須先獲取到聚合根再通過聚合根完成業(yè)務邏輯,最終通過資源庫持久化聚合根。

為什么需要將DO轉PO,這是必須要做的事情嗎?

如果我們選擇關系型數(shù)據(jù)庫持久化聚合根,那么就可能需要將聚合根拆分存儲到多個表,并且對于枚舉類型我們也需要轉成數(shù)值類型再存儲。基于這些場景就需要將聚合根轉為PO再調用對應表的DAO存儲到數(shù)據(jù)庫中。

為什么需要將DO轉DTO?

除了我們必須要遵守不暴露聚合根內部結構給外部之外,前端需要的數(shù)據(jù)也是不一樣的,比如我們需要將枚舉類型字段拆成值和名稱兩個字段,以及需要屏蔽一些字段。

為了不暴露聚合根內部結構,聚合根應只暴露GET方法,用于外部獲取字段值,同時使用Builder模式提供builder方法給資源庫(使用裝配器)將PO轉為聚合根。對于實體也可以這樣做,而值對象只提供所有參數(shù)的構造方法和GET方法。當然了,使用哪種做法都沒有錯。

對于通用的轉換操作我們?yōu)槊總€聚合根提供一個實現(xiàn)將DO(聚合根)轉為DTO的裝配器(轉換器)、以及一個實現(xiàn)PO和DO相互轉換的裝配器(轉換器)。

基于這種實現(xiàn),筆者也尋找過能夠解決這些繁瑣操作提升工作效率的方法,我們試過用mapstruct框架,但mapstruct也只適用于簡單的聚合根,對于復雜內部結構的聚合根映射也需要寫一堆注解,工作量沒有減少反而增加了問題排查的難度。使用Spring提供的屬性拷貝工作類也是一樣的,無法解決問題。

優(yōu)化聚合根的持久化性能

對于使用關系型數(shù)據(jù)庫持久化聚合根的場景,在“只能通過Repository的save方法持久化聚合根”這個約束下,save方法在性能上是有非常大的損耗的,因為更新一個聚合根需要同時更新聚合根下的實體。為了降低性能影響,可在更新之前對比一下內存中的快照,只對有更新的實體執(zhí)行更新操作,筆者單獨寫了一篇文章介紹如何實現(xiàn):《DDD資源庫Repository的性能優(yōu)化》。

如今分布式數(shù)據(jù)庫已經(jīng)成熟,不建議在新項目中引入分庫分表ORM框架以及分布式事務框架,更不建議使用分庫分表,這些應該交由底層數(shù)據(jù)庫完成,或增加一層代理完成。

總結

因為DDD缺少權威性的實踐指導和代碼約束,我們只能是通過實踐慢慢積累經(jīng)驗。個人的理解也并非完全正確的,對于限界上下文的劃分,我們只是憑經(jīng)驗劃分,但又缺少經(jīng)驗,新業(yè)務也處于不斷摸索狀態(tài),現(xiàn)在的限界上下文劃分、建模不代表將來不會推倒重來。

參考文獻:

領域驅動實戰(zhàn)思考(三):DDD的分段式協(xié)作設計

領域驅動設計(DDD)在美團點評業(yè)務系統(tǒng)的實踐

領域驅動設計在愛奇藝打賞業(yè)務的實踐

《領域驅動設計(Thoughtworks洞見)》

本文轉載自微信公眾號「Java藝術」,可以通過以下二維碼關注。轉載本文請聯(lián)系Java藝術公眾號。

 

責任編輯:武曉燕 來源: Java藝術
相關推薦

2022-01-13 08:13:14

Vue3 插件Vue應用

2024-11-25 09:10:03

2012-05-07 10:40:57

阿里巴巴去IOE

2021-10-28 07:10:21

rollupPlugin插件編寫

2022-06-02 08:37:10

架構DDDMVC

2021-08-15 22:52:30

前端H5拼圖

2021-06-09 15:55:34

Oracle賬號鎖定

2017-10-24 11:39:29

銀行轉賬數(shù)據(jù)庫分布式事務

2020-09-08 18:37:49

TypeScript開發(fā)前端

2021-07-12 07:33:31

Nacos微服務管理

2014-06-11 10:29:03

2016-08-25 20:55:19

微服務架構發(fā)布

2025-10-31 07:10:00

裝飾器Python代碼

2012-02-01 14:28:03

Java線程

2019-05-07 11:24:07

ReactJavascriptTypescript

2022-02-13 23:00:48

前端微前端qiankun

2025-01-16 10:46:31

2025-06-27 06:30:08

2024-06-12 09:06:48

2025-04-02 07:30:37

LLMDify應用
點贊
收藏

51CTO技術棧公眾號

日本成人精品在线| 欧美成人激情免费网| 日本精品一区二区三区不卡无字幕| 毛片基地在线观看| 精品欧美久久| 3atv一区二区三区| 国产欧美日韩小视频| 青青操视频在线| 精品一区二区三区在线观看| 久久久久免费视频| 色欲av无码一区二区三区| 国产精品亚洲成在人线| 亚洲一区二区精品3399| 视频在线99| 亚洲精品视频专区| 日本91福利区| 国模gogo一区二区大胆私拍| 福利视频第一页| 久久av国产紧身裤| 在线成人av网站| 国内外成人激情视频| 四虎亚洲精品| 国产免费久久精品| 久久av一区二区| 精品人妻午夜一区二区三区四区| 亚洲欧美不卡| 欧美国产日韩一区二区在线观看| 久久丫精品忘忧草西安产品| 91综合精品国产丝袜长腿久久| 欧美在线一区二区三区| 日本欧美黄色片| 中日韩高清电影网| 国产精品女人毛片| 老司机精品福利在线观看| 成 人片 黄 色 大 片| 麻豆成人在线观看| 国产成人啪精品视频免费网| 国产主播在线播放| 欧美精品播放| 欧美超级乱淫片喷水| 亚洲女优在线观看| 精品国产乱码久久久久久果冻传媒| 精品成人a区在线观看| 国产精品久久久久久9999| 性欧美videohd高精| 亚洲va欧美va人人爽午夜| 欧美日韩午夜爽爽| 精品麻豆一区二区三区| 中文字幕在线观看不卡| 色姑娘综合网| 国产视频网站在线| 久久久久久夜精品精品免费| 久久精品女人的天堂av| 免费看日韩av| 成人18视频在线播放| 国产91精品入口17c| 国产成人精品av在线观| 国产麻豆精品久久一二三| 亚洲a区在线视频| 国产美女主播在线观看| 卡一卡二国产精品| 国产日韩中文字幕| 一区不卡在线观看| 激情图片小说一区| 96精品久久久久中文字幕| 国产口爆吞精一区二区| 国产毛片精品国产一区二区三区| 91老司机在线| 精品人妻一区二区三区换脸明星 | 成年人免费大片| 成人国产二区| 欧洲精品一区二区三区在线观看| 日本a√在线观看| 人人精品久久| 日韩精品一区二| 漂亮人妻被黑人久久精品| 婷婷精品在线观看| 国产一区二区动漫| 网爆门在线观看| 在线一区电影| 97在线日本国产| 免费看av在线| 国产一区二区视频在线| 国产精品美女诱惑| 国产中文字幕在线观看| 国产精品久久久久久亚洲毛片 | 亚洲色图27p| 欧美1区2区视频| 51久久精品夜色国产麻豆| 超碰在线97观看| 国产成人午夜99999| 好吊色欧美一区二区三区| 久久免费看视频| 亚洲色图一区二区| av日韩一区二区三区| 偷拍视频一区二区三区| 3d动漫精品啪啪| 真人bbbbbbbbb毛片| 久久人人88| **欧美日韩vr在线| 一级特黄aaa大片| av成人动漫在线观看| 亚洲精品国产精品国自产| 伊人影院在线视频| 欧美在线|欧美| 91成人在线观看喷潮蘑菇| 国产精品一国产精品| 欧美人与性动交| 日本妇乱大交xxxxx| 成人午夜精品一区二区三区| 水蜜桃一区二区三区| 国产经典三级在线| 欧美精品vⅰdeose4hd| 欧洲一级黄色片| 女主播福利一区| 国产精品一区二区电影| 视频二区在线| 亚洲一区av在线| 涩涩网站在线看| 欧美美乳视频| 777午夜精品福利在线观看| 国产美女无遮挡永久免费| 久久精品综合网| 日本在线xxx| 风间由美一区二区av101| 日韩视频免费观看| 中文字幕av网站| 久久久久久久久久久久久久久99| 亚洲理论电影在线观看| 9999在线精品视频| 在线观看精品国产视频| 黄色在线视频网址| 99精品视频在线免费观看| 三级在线免费观看| 亚洲国产91视频| 中文字幕欧美精品在线 | 欧美欧美欧美欧美首页| 高潮毛片无遮挡| 蜜桃av综合| 久久偷窥视频| 中文字幕色婷婷在线视频| 亚洲第一精品自拍| 久一视频在线观看| 国产jizzjizz一区二区| 伊人再见免费在线观看高清版| 亚洲精品成a人ⅴ香蕉片| 日韩在线免费视频| 国产乱码在线观看| 国产精品天天看| 午夜久久福利视频| 在线观看国产精品入口| 亚洲一区二区中文| 午夜在线激情影院| 精品久久一区二区三区| 久久免费视频99| 成人av网站在线观看免费| 霍思燕三级露全乳照| 欧美丝袜美腿| 日本一区二区不卡| 第九色区av在线| 欧美日韩美女一区二区| 三级影片在线观看| 国产一区二区美女诱惑| 激情五月婷婷六月| 精品少妇3p| 国产激情视频一区| 3p视频在线观看| 欧美一区二区黄色| 国产成人精品亚洲男人的天堂| 91丝袜国产在线播放| 欧美性猛交久久久乱大交小说| 国产欧美高清视频在线| 国产精品三级美女白浆呻吟| 秋霞成人影院| 欧美成人r级一区二区三区| 天天操天天射天天爽| 国产欧美日韩激情| 男女视频在线观看网站| 国产精品s色| 六十路精品视频| 久久精品超碰| 欧美激情精品久久久久久免费印度 | 麻豆传媒视频在线| 欧美www视频| 亚洲天堂男人av| 亚洲婷婷综合色高清在线| 在线播放av网址| 三级欧美在线一区| 99re8这里只有精品| 欧美黑人巨大videos精品| 国产成人一区二区三区小说| 老司机在线永久免费观看| 欧美变态口味重另类| 国产又大又粗又爽| 亚洲欧洲无码一区二区三区| 超碰caoprom| 秋霞午夜av一区二区三区| 黄色一级大片免费| 国产一区三区在线播放| 91观看网站| 亚洲四虎影院| 国产+人+亚洲| 日本三级视频在线观看| 日韩av在线免费| 国产农村老头老太视频| 色国产综合视频| 久久综合久久鬼| 亚洲色图视频网| 日韩免费成人av| 99久久精品一区| 97人人模人人爽人人澡| 日韩激情在线观看| 男女超爽视频免费播放| 亚洲中无吗在线| 色姑娘综合av| 婷婷综合成人| 国产一区二区三区黄| 欧美视频精品| 国产极品jizzhd欧美| 日本乱码一区二区三区不卡| 欧美成年人视频网站| 99精品老司机免费视频| 精品无人国产偷自产在线| 亚洲av无码乱码国产精品| 欧美日韩电影一区| 欧美男人天堂网| 色综合天天综合在线视频| 久久精品这里有| 亚洲制服丝袜av| 午夜精品福利在线视频| 国产精品天天看| 国产一区二区三区四区在线| 91麻豆产精品久久久久久| a级片在线观看视频| 国产福利一区在线| 中文国产在线观看| 激情综合色播激情啊| 欧美在线aaa| 日本v片在线高清不卡在线观看| 色综合av综合无码综合网站| 一区二区三区国产在线| 亚洲精品蜜桃久久久久久| 欧美精品导航| 成品人视频ww入口| 亚洲承认在线| 欧美精品久久久久久久自慰| 影音先锋中文字幕一区| 又大又硬又爽免费视频| 国产精品theporn| 国产免费黄色一级片| 亚洲精品婷婷| 欧美 日韩 国产 高清| 在线亚洲观看| 久久综合久久色| 日韩精品高清不卡| 天天干天天爽天天射| 久久精品国产亚洲aⅴ| 天堂在线中文在线| 国产自产高清不卡| 97精品人人妻人人| 99re热这里只有精品免费视频 | 99在线精品视频免费观看20| 制服丝袜中文字幕一区| 99热在线只有精品| 日韩视频永久免费| 欧美一级免费片| 亚洲欧美成人精品| 在线激情小视频| 久久视频免费在线播放| 国产深夜视频在线观看| 欧美亚洲午夜视频在线观看| 日本少妇一区| 91日韩在线播放| 99久久婷婷国产综合精品青牛牛| 国产亚洲精品美女久久久m| 女人av一区| 一区二区三区一级片| 精品福利电影| 国产三级日本三级在线播放| 久久99日本精品| 欧美xxxx×黑人性爽| 久久精品视频免费| 日韩成人短视频| 欧美日韩亚洲高清| 亚洲天堂视频网| 亚洲国产成人久久综合一区| 国产午夜在线观看| 九九热最新视频//这里只有精品 | dy888夜精品国产专区| 日韩av中文字幕一区| 杨幂一区欧美专区| 亚洲三级视频| 国产又粗又长又爽又黄的视频| 成人精品视频一区二区三区 | 天堂8在线视频| 中文字幕自拍vr一区二区三区| 日本在线视频网址| 国产精品大陆在线观看| 亚洲精品观看| 亚洲精品一卡二卡三卡四卡| 激情欧美丁香| 久久99爱视频| 91在线porny国产在线看| 登山的目的在线| 欧美午夜激情在线| 精品人妻一区二区三区蜜桃| 亚洲午夜精品久久久久久久久久久久 | 欧美专区视频| 天堂av一区二区| 宅男噜噜噜66国产日韩在线观看| 手机免费看av网站| 久久久精品免费网站| 日韩av电影网| 日韩午夜中文字幕| 日韩av中文| 国产91在线播放精品91| 国产精品tv| 免费成人进口网站| 蜜臀av性久久久久蜜臀av麻豆| 免费的av网站| 亚洲午夜免费电影| 国产按摩一区二区三区| 最新国产成人av网站网址麻豆| 亚洲欧洲自拍| 国产一区二区自拍| 你懂的国产精品永久在线| 日韩不卡一二三| 久久久久久久久伊人| 精品人妻一区二区三区免费看 | 成人短视频在线观看| 国产福利视频一区二区| 色狼人综合干| 国产一区二区网| 成人av免费观看| 精品无码一区二区三区电影桃花 | 欧美喷水一区二区| 免费在线视频你懂得| 2019中文字幕在线观看| 久久久免费毛片| www.日本在线播放| 99在线热播精品免费| 国产一级av毛片| 欧美精品一区二区三区蜜桃视频| 91精品国产91久久久久久青草| 91久久精品美女高潮| 亚洲综合中文| 麻豆av免费看| 亚洲成人av在线电影| 三级网站在线看| 136fldh精品导航福利| 亚洲另类av| 手机看片福利日韩| 国产精品免费观看视频| 一级二级三级视频| 久久精品国亚洲| 日韩精品三级| 青青青在线视频播放| heyzo一本久久综合| 免费av网站在线| 亚洲午夜小视频| 久久亚洲人体| 大片在线观看网站免费收看| 成人av在线播放网站| 4438国产精品一区二区| 国产亚洲综合久久| 高清一区二区三区av| 久久亚洲国产成人精品无码区| k8久久久一区二区三区| 国产伦精品一区二区三区视频我| 在线亚洲午夜片av大片| 国产一区 二区| 国产日韩欧美精品在线观看| 久久午夜色播影院免费高清 | 亚洲第一男人天堂| 亚洲国产成人二区| 亚洲精品白虎| 国产成人啪午夜精品网站男同| 精品91久久久| 中文字幕v亚洲ⅴv天堂| 国产精选久久| 免费观看美女裸体网站| 中文字幕成人在线观看| 成人黄色免费视频| 欧美中文字幕在线观看| 99re66热这里只有精品8| 91人人澡人人爽| 在线看不卡av| 日本小视频在线免费观看| 蜜桃狠狠色伊人亚洲综合网站| 美日韩一区二区| 久久精品视频久久| 在线观看国产欧美| 成人看片黄a免费看视频| 手机看片福利盒子久久| 亚洲一区二区精品久久av| 午夜在线免费观看视频| 国精产品一区二区| 国内精品久久久久影院色| 国产综合精品视频| 欧美成人在线网站|