<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      產品代碼都給你看了,可別再說不會DDD(五):請求處理流程

      這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕云。本文章系列以一個真實的并已成功上線的軟件項目——碼如云https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取舍。

      本系列包含以下文章:

      1. DDD入門
      2. DDD概念大白話
      3. 戰略設計
      4. 代碼工程結構
      5. 請求處理流程(本文)
      6. 聚合根與資源庫
      7. 實體與值對象
      8. 應用服務與領域服務
      9. 領域事件
      10. CQRS

      案例項目介紹

      既然DDD是“領域”驅動,那么我們便不能拋開業務而只講技術,為此讓我們先從業務上了解一下貫穿本文章系列的案例項目 —— 碼如云(不是馬云,也不是碼云)。如你已經在本系列的其他文章中了解過該案例,可跳過。

      碼如云是一個基于二維碼的一物一碼管理平臺,可以為每一件“物品”生成一個二維碼,并以該二維碼為入口展開對“物品”的相關操作,典型的應用場景包括固定資產管理、設備巡檢以及物品標簽等。

      在使用碼如云時,首先需要創建一個應用(App),一個應用包含了多個頁面(Page),也可稱為表單,一個頁面又可以包含多個控件(Control),比如單選框控件。應用創建好后,可在應用下創建多個實例(QR)用于表示被管理的對象(比如機器設備)。每個實例均對應一個二維碼,手機掃碼便可對實例進行相應操作,比如查看實例相關信息或者填寫頁面表單等,對表單的一次填寫稱為提交(Submission);更多概念請參考碼如云術語

      在技術上,碼如云是一個無代碼平臺,包含了表單引擎、審批流程和數據報表等多個功能模塊。碼如云全程采用DDD完成開發,其后端技術棧主要有Java、Spring Boot和MongoDB等。

      碼如云的源代碼是開源的,可以通過以下方式訪問:

      碼如云源代碼:https://github.com/mryqr-com/mry-backend

      請求處理流程

      在上一篇代碼工程結構中,我們從宏觀層面講到了DDD項目的目錄結構,但并未觸及到實際的代碼。在本文中,我們將深入到代碼中,逐一講解DDD中對各種請求類型的典型處理流程。

      在本系列的DDD概念大白話我們提到,DDD中的所有組件都是圍繞著聚合根展開的,其中有些本身即是聚合根的一部分,比如實體和值對象;有些是聚合根的客戶,比如應用服務;有些則是對聚合根的輔助或補充,比如領域服務和工廠。反觀當下流行的各種軟件架構,無論是分層架構、六邊形架構還是整潔架構,它們都有一個共同點,即在架構中心都有一個核心存在,這個核心正是領域模型,而DDD的聚合根則存在于領域模型之中。

      不難看出,既然每種架構中都有為領域模型預留的位置,這也意味著DDD可采用任何一種軟件架構。事實也的確如此,DDD并不要求采用哪種特定架構,如果你真要說DDD項目應該采用某種架構的話,那么應該“以領域模型為中心的軟件架構”。

      如果我們把軟件系統當做一個黑盒的話,其外界是各種形態的客戶端,比如瀏覽器,手機APP或者第三方調用方等,盒子內部則是我們精心構建的領域模型。不過,領域模型是不能直接被外界訪問的,主要原因有以下兩點:

      • 客戶端的演進和領域模型的演進是不同步的,比如網頁端所需要展示的信息量比手機端更多,但是他們所使用的領域模型卻是相同的,因此在建模時我們通常會將領域模型和客戶端解耦開來,以利于各自的建模和演進
      • 軟件除了處理領域模型這種業務復雜度之外,還需要處理技術復雜度,以及業務和技術的銜接復雜度,比如有些請求通過HTTP協議完成,而有些則通過RPC完成,因此除了領域模型,我們還需要適配各種形式的外部客戶端

      接下來,讓我們來看看DDD項目是如何銜接外部請求和內部領域模型的。既然聚合根是領域模型中的一等公民,那么按照對聚合根的操作類型不同,DDD項目中主要存在以下4種類型的請求:

      • 聚合根創建流程
      • 聚合根更新流程
      • 聚合根刪除流程
      • 查詢流程

      咋一看,你可能會說這不就是CRUD么?本質上這的確是CRUD,但是這里的CRUD可不是僅僅操作數據庫那么簡單,你如果閱覽過本系列的上一篇代碼工程結構的話,便知道在碼如云中領域模型的代碼量占比遠遠高出數據庫訪問相關的代碼量。

      本文主要講解DDD對請求的處理流程,并不講解聚合根本身的設計和實現,而是假設聚合根(以及領域模型中的工廠和領域服務等)已經實現就位了,關于聚合根本身的講解請參考本系列的聚合根與資源庫一文。此外,為了突出重點,本文只著重講解請求處理流程的主干,而忽略與之關系不大的其他細節,比如我們將忽略應用服務中的事務處理和權限管理等功能,為此讀者可參考應用服務與領域服務。

      聚合根創建流程

      聚合根的創建通常通過工廠類完成,請求流經路線為:控制器(Controller) -> 應用服務(Application Service) -> 工廠(Factory) -> 資源庫(Repository)。

      在碼如云中,當用戶提交表單后,系統后臺將創建一份提交(Submission),這里的Submission便是一個聚合根對象。在整個“創建Submission”的處理流程中,請求先通過HTTP協議到達Spring MVC中的Controller:

      //SubmissionController
      
      @PostMapping
      @ResponseStatus(CREATED)
      public ReturnId newSubmission(@RequestBody @Valid NewSubmissionCommand command,
                                    @AuthenticationPrincipal User user) {
          String submissionId = submissionCommandService.newSubmission(command, user);
          return returnId(submissionId);
      }

      源碼出處:com/mryqr/core/submission/SubmissionController.java

      Controller的作用只是為了銜接技術和業務,因此其邏輯應該相對簡單,在本例中,SubmissionControllernewSubmission()方法僅僅將請求代理給應用服務SubmissionCommandService即完成了其自身的使命。這里的NewSubmissionCommand表示命令對象,用于攜帶請求數據,比如對于“創建Submission”來說,NewSubmissionCommand對象中至少應該包含表單的提交內容等數據。命令對象是外部客戶端傳入的數據,因此需要將其與領域模型解耦,也即命令對象不能進入到領域模型的內部,其所能到達的最后一站是應用服務。

      處理流程的下一站是應用服務,應用服務是整個領域模型的門面,無論什么類型的客戶端,只要業務用例相同,那么所調用的應用服務的方法也應相同,也即應用服務和技術設施也是解耦的。

      //SubmissionCommandService
      
      @Transactional
      public String newSubmission(NewSubmissionCommand command, User user) {
          AppedQr appedQr = qrRepository.appedQrById(command.getQrId());
          App app = appedQr.getApp();
          QR qr = appedQr.getQr();
      
          Page page = app.pageById(command.getPageId());
          SubmissionPermissions permissions = permissionChecker.permissionsFor(user, appedQr);
          permissions.checkPermissions(app.requiredPermission(), page.requiredPermission());
      
          Set<Answer> answers = command.getAnswers();
          Submission submission = submissionFactory.createNewSubmission(
                  answers,
                  qr,
                  page,
                  app,
                  permissions.getPermissions(),
                  command.getReferenceData(),
                  user
          );
      
          submissionRepository.houseKeepSave(submission, app);
          log.info("Created submission[{}].", submission.getId());
      
          return submission.getId();
      }

      源碼出處:com/mryqr/core/submission/command/SubmissionCommandService.java

      在以上的SubmissionCommandService應用服務中,首先做權限檢查,然后調用工廠SubmissionFactory.createNewSubmission()完成Submission的創建,最后調用資源庫SubmissionRepository.houseKeepSave()將新建的Submission持久化到數據庫中。從中可見,應用服務主要用于協調各方以完成一個業務用例,其本身并不包含業務邏輯,業務邏輯在工廠中完成。

      //SubmissionFactory
      
      public Submission createNewSubmission(Set<Answer> answers,
                                            QR qr,
                                            Page page,
                                            App app,
                                            Set<Permission> permissions,
                                            String referenceData,
                                            User user) {
          if (page.isOncePerInstanceSubmitType()) {
              submissionRepository.lastInstanceSubmission(qr.getId(), page.getId())
                      .ifPresent(submission -> {
                          throw new MryException(SUBMISSION_ALREADY_EXISTS_FOR_INSTANCE,
                                  "當前頁面不支持重復提交,請嘗試更新已有表單。",
                                  mapOf("qrId", qr.getId(),
                                          "pageId", page.getId()));
                      });
          }
      
          //...此處忽略更多業務邏輯
      
          //只有需要登錄的頁面才記錄user
          User finalUser = page.requireLogin() ? user : ANONYMOUS_USER;
          Map<String, Answer> checkedAnswers = submissionDomainService.checkAnswers(answers,
                  qr,
                  page,
                  app,
                  permissions);
      
          return new Submission(checkedAnswers,
                  page.getId(),
                  qr, app,
                  referenceData,
                  finalUser);
      }

      源碼出處:com/mryqr/core/submission/domain/SubmissionFactory.java

      雖然工廠用于創建聚合根,但并不是直接調用聚合根的構造函數那么簡單,從SubmissionFactory.createNewSubmission()可以看出,在創建Submission之前,需要根據表單類型檢查是否可以創建新的Submission,而這正是業務邏輯的一部分。因此,工廠也屬于領域模型的一部分,本質上工廠可以認為是一種特殊形式的領域服務。

      請求流程的最后,應用服務調用資源庫submissionRepository.houseKeepSave()完成對新建Submission的持久化。更多關于資源庫的內容,請參考聚合根與資源庫一文。

      聚合根更新流程

      對聚合根的更新流程通??梢酝ㄟ^“經典三部曲”完成:

      1. 調用資源庫獲得聚合根
      2. 調用聚合根上的業務方法,完成對聚合根的更新
      3. 再次調用資源庫保存聚合根

      此時的請求流經路線為:控制器(Controller) -> 應用服務(Application Service) -> 資源庫(Repository) -> 聚合根(Aggregate Root)。

      碼如云中,當表單開啟了審批功能過后,管理員可對Submission進行審批操作,本質上則是在更新Submission。在“審批Submission”的過程中,請求依然是首先到達Controller:

      //SubmissionController
      
      @ResponseStatus(CREATED)
      @PostMapping(value = "/{submissionId}/approval")
      public ReturnId approveSubmission(@PathVariable("submissionId") @SubmissionId @NotBlank String submissionId,
                                        @RequestBody @Valid ApproveSubmissionCommand command,
                                        @AuthenticationPrincipal User user) {
          submissionCommandService.approveSubmission(submissionId, command, user);
          return returnId(submissionId);
      }

      源碼出處:com/mryqr/core/submission/SubmissionController.java

      與“創建聚合根”相似,SubmissionController直接將請求代理給應用服務SubmissionCommandService.approveSubmission()

      //SubmissionCommandService
      
      @Transactional
      public void approveSubmission(String submissionId,
                                    ApproveSubmissionCommand command,
                                    User user) {
          Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
      
          App app = appRepository.cachedById(submission.getAppId());
          Page page = app.pageById(submission.getPageId());
          SubmissionPermissions permissions = permissionChecker.permissionsFor(user,
                  app,
                  submission.getGroupId());
          permissions.checkCanApproveSubmission(submission, page, app);
      
          submission.approve(command.isPassed(),
                  command.getNote(),
                  page,
                  user);
      
          submissionRepository.houseKeepSave(submission, app);
      
          log.info("Approved submission[{}].", submissionId);
      }

      源碼出處:com/mryqr/core/submission/command/SubmissionCommandService.java

      應用服務SubmissionCommandService先通過資源庫SubmissionRepositorybyIdAndCheckTenantShip()方法獲取到需要操作的Submission,然后進行權限檢查,再調用Submission.approve()方法完成對Submission的更新,最后調用資源庫SubmissionRepositoryhouseKeepSave()方法將更新后的Submission保存到數據庫。這里的重點在于:需要保證所有的業務邏輯均放在Submission.approve()中:

      //Submission
      
      public void approve(boolean passed,
                          String note,
                          Page page,
                          User user) {
      
          if (isApproved()) {
              throw new MryException(SUBMISSION_ALREADY_APPROVED,
                      "無法完成審批,先前已經完成審批。",
                      "submissionId", this.getId());
          }
      
          this.approval = SubmissionApproval.builder()
                  .passed(passed)
                  .note(note)
                  .approvedAt(now())
                  .approvedBy(user.getMemberId())
                  .build();
      
          raiseEvent(new SubmissionApprovedEvent(this.getId(),
                  this.getQrId(),
                  this.getAppId(),
                  this.getPageId(),
                  this.approval,
                  user));
      
          addOpsLog(passed ?
                  "審批" + page.approvalPassText() :
                  "審批" + page.approvalNotPassText(), user);
      }

      源碼出處:com/mryqr/core/submission/domain/Submission.java

      可以看到,Submission.approve()先檢查Submission是否已經被審批過了,如果尚未審批才繼續審批操作,審批過程還會發出“提交已審批”(SubmissionApprovedEvent)領域事件(更多關于領域事件的內容,請參考本系列的領域事件一文)。Submission.approve()中的代碼量雖然不多,但是卻體現了核心的業務邏輯:“已經完成審批的提交不能再次審批”。

      當然,并不是所有的業務用例都適合“經典三部曲”,有時聚合根自身無法完成所有的業務邏輯,此時我們則需要借助領域服務(Domain Service)來完成請求的處理。比如,常見的使用領域服務的場景是需要進行跨聚合查詢的時候。此時的請求流經路線則為:控制器(Controller) -> 應用服務(Application Service) -> 資源庫(Repository) -> 聚合根(Aggregate Root) ->領域服務(Domain Service)。

      在碼如云中,管理員可以對既有的Submission進行編輯更新,但是由于更新時可能涉及到檢查手機號或者郵箱等控件填值的唯一性,因此在更新時需要跨Submission進行查詢,此時光靠Submission自身便無法完成了,為此我們可以創建領域服務SubmissionDomainService用于跨Submission操作:

      //SubmissionCommandService
      
      @Transactional
      public void updateSubmission(String submissionId,
                                   UpdateSubmissionCommand command,
                                   User user) {
      
          Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
          AppedQr appedQr = qrRepository.appedQrById(submission.getQrId());
          App app = appedQr.getApp();
          QR qr = appedQr.getQr();
      
          Page page = app.pageById(submission.getPageId());
          SubmissionPermissions permissions = submissionPermissionChecker.permissionsFor(user,
                  app,
                  submission.getGroupId());
          permissions.checkCanUpdateSubmission(submission, page, app);
      
          submissionDomainService.updateSubmission(submission,
                  app,
                  page,
                  qr,
                  command.getAnswers(),
                  permissions.getPermissions(),
                  user
          );
      
          submissionRepository.houseKeepSave(submission, app);
          log.info("Updated submission[{}].", submissionId);
      }

      源碼出處:com/mryqr/core/submission/command/SubmissionCommandService.java

      在本例中,應用服務SubmissionCommandService并未直接調用聚合根Submission中的方法,而是將Submission作為參數傳入了領域服務SubmissionDomainServiceupdateSubmission()方法中,在SubmissionDomainService完成了對Submission的更新后,SubmissionCommandService再調用SubmissionRepository.houseKeepSave()方法將Submission保存到數據庫中。SubmissionDomainService.updateSubmission()實現如下:

      //SubmissionDomainService
          
      public void updateSubmission(Submission submission,
                                   App app,
                                   Page page,
                                   QR qr,
                                   Set<Answer> answers,
                                   Set<Permission> permissions,
                                   User user) {
      
          Map<String, Answer> checkedAnswers = checkAnswers(answers,
                  qr,
                  page,
                  app,
                  submission.getId(),
                  permissions);
      
          Set<String> submittedControlIds = answers.stream()
                  .map(Answer::getControlId)
                  .collect(toImmutableSet());
      
          submission.update(submittedControlIds, checkedAnswers, user);
      }

      源碼出處:com/mryqr/core/submission/domain/answer/SubmissionDomainService.java

      可以看到,SubmissionDomainService.updateSubmission()首先調用業務方法checkAnswers()對表單內容進行檢查(其中便包含上文提到的對手機號或郵箱的重復性檢查),再調用Submission.update()以完成對Submission的更新,相當于SubmissionDomainServiceSubmission做了業務上的加工。

      這里,領域服務SubmissionDomainService的職責范圍僅包含對聚合根Submission的更新,并不負責持久化Submission,持久化的職責依然在應用服務SubmissionCommandService上。這種方式的好處在于:(1)與“經典三部曲”保持一致,將所有持久化操作均集中到應用服務中,不至于過于分散;(2)使領域服務的職責盡量單一。

      聚合根刪除流程

      聚合根刪除流程相對簡單,此時的請求流經路線為:控制器(Controller) -> 應用服務(Application Service) -> 資源庫(Application Service) -> 聚合根(Aggregate Root) 。

      刪除請求首先到達Controller:

      //SubmissionController
      
      @DeleteMapping(value = "/{submissionId}")
      public ReturnId deleteSubmission(@PathVariable("submissionId") @SubmissionId @NotBlank String submissionId,
                                       @AuthenticationPrincipal User user) {
          submissionCommandService.deleteSubmission(submissionId, user);
          return returnId(submissionId);
      }

      源碼出處:com/mryqr/core/submission/SubmissionController.java

      Controller將請求進一步代理給應用服務SubmissionCommandService

      //SubmissionCommandService
      
      @Transactional
      public void deleteSubmission(String submissionId, User user) {
          Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);
          Group group = groupRepository.cachedById(submission.getGroupId());
          managePermissionChecker.checkCanManageGroup(user, group);
      
          submission.onDelete(user);
          submissionRepository.delete(submission);
          log.info("Deleted submission[{}].", submissionId);
      }

      源碼出處:com/mryqr/core/submission/command/SubmissionCommandService.java

      應用服務SubmissionCommandService通過SubmissionRepository加載出需要刪除的Submission后,再調用Submission.onDelete()以完成刪除前的一些操作,在本例中onDelete()將發出“提交已刪除”(SubmissionDeletedEvent)領域事件:

      //Submission
          
      public void onDelete(User user) {
          raiseEvent(new SubmissionDeletedEvent(this.getId(),
                  this.getQrId(),
                  this.getAppId(),
                  this.getPageId(),
                  user));
      }

      源碼出處:com/mryqr/core/submission/domain/Submission.java

      最后,應用服務SubmissionCommandService調用SubmissionRepository.delete()完成對聚合根的刪除操作。

      查詢流程

      在本系列的CQRS一文中,我們將專門講到在DDD中如何做查詢操作。

      總結

      在本文中,我們分別對聚合根的新建、更新和刪除的典型請求處理流程做了詳細介紹。在這些流程中,我們以聚合根為中心,圍繞之形成了恰如其分的軟件架構。在下一篇聚合根與資源庫中,我們將對聚合根本身的設計與實現做詳細講解。

      posted @ 2023-09-03 10:31  無知者云  閱讀(1283)  評論(1)    收藏  舉報
      主站蜘蛛池模板: 综合色一色综合久久网| 国产在线精品一区二区夜色| 成人网站免费看黄a站视频| 国产在线精品一区二区夜色| 色悠悠国产精品免费观看| 无码精品人妻一区二区三区中| 欧洲亚洲精品免费二区| 亚洲日韩亚洲另类激情文学 | 精品午夜福利短视频一区| 桐庐县| 国产亚洲精品视频一二区| 日本三级理论久久人妻电影| 日产精品99久久久久久| 成人伊人青草久久综合网| 十四以下岁毛片带血a级| 久热这里只有精品12| 色偷偷亚洲精品一区二区| 久久久久久曰本av免费免费| 国产av一区二区三区久久| 国产精品中文av专线| 欧美丰满熟妇hdxx| 麻豆麻豆麻豆麻豆麻豆麻豆| 草裙社区精品视频播放| 中文字幕亚洲制服在线看| 52熟女露脸国语对白视频| 日韩精品专区在线影院重磅| 亚洲人成小说网站色在线| 凸凹人妻人人澡人人添| 97久久精品人人做人人爽| 女人扒开的小泬高潮喷小| 精品国产迷系列在线观看| 亚洲熟妇熟女久久精品一区 | 疯狂做受XXXX高潮国产| 成人无遮挡裸免费视频在线观看| 中日韩黄色基地一二三区| 婷婷四房播播| 西乌珠穆沁旗| 天天躁夜夜躁狠狠喷水| 国产精品午夜福利在线观看| 狠狠综合久久av一区二| 亚洲一区二区精品偷拍|