Stories

Detail Return Return

DDD重構項目 - Stories Detail

雲圖庫項目ddd重構


  • 把原項目複製一份,用副本進行項目重構

  • 原結構為 com.yupi.yupicturebackend.xxx,保留 com.yupi,新建一個和 yupicturebackend 包同級的包來當作ddd重構包

  • 把主類 xxxApplication 先拖到新建的根包中。因為主類會掃描相同包下的路徑,我們要在重構的ddd的項目架構中運行程序,所以先將原包下的主程序移動到當前包下,不然項目沒法啓動。


先重構*** infrastructure 基礎設施層***,因為代碼通用、調用廣泛,很多基礎的代碼會被領域層等等的業務依賴。

infrastructure 基礎設施層:repository imlp(數據訪問層實現,也就是mapper、dao、repository層) 、緩存、OSS、第三方工具等

  • annotation 包直接拖,先不用管可能調用別的,這是一個很基礎的代碼,自己寫的註解適用比較廣泛。

  • aop , api, common , config , exception , utils 這些也都拖過來

  • mapper 拖過來。因為對mapper在config包中寫了一個配置類,所以要去修改對應路徑。

    雖然mapper是用 Mybatis-X 插件自動生成的,但是生成的mapper類繼承了 BaseMapper<T> 類,其實是自動交給了mybatis-plus去實現的,所以也看成是實現類。

    也就是 domain領域層的 依賴倒置原則 ,具體的技術實現全部下放到 infrastructure基礎設施層,domain層只留接口調用,接口提供方是怎麼實現的無需理會。

  • 然後將manager 包下的 CosManager 類可以移過來。沒有調用mapper、沒有調用service,就是一個很乾淨的、可以獨立於項目去提供服務的一個類。所以可以拖到api目錄下。(如果有需要也可以更名為 CosApi 。)

  • 可以發現這些包都是一些通用代碼,全局可用,都放在基礎設施類裏等待調用。

注意是 直接拖動, 不要複製,複製可能導致代碼中的路徑和包名等和原位置一致,直接拖動的話會自動重構為當前目錄。

ok,至此,基礎設施層重構完畢。


層層遞進,接下來是重構 domain領域層

領域層包含:聚合(domain service)、entity實體、Value Object值對象、repository api。

!!! 一個領域一個領域地去重構,而不是一次性把多個領域的代碼同時改造,這樣出了問題不好還原。

===

一個領域一個領域地拆的同時,回顧當初的開發流程 :先開發的 model,再開發的 service,最後開發 controller。所以,先確定了哪個領域,然後按照開發流程順序解構原包。但是原包中肯定不可能只包含我們正在重構的領域的代碼,秉持拆一個包就拆乾淨的原則,其他代碼該移動到哪裏就直接一步到位即可。

那麼現在就可以發現(初見端倪),以及不按照目標重構了,而是按開發流程重構。因為按照目標重構(解讀一下什麼叫按照目標重構:就是在設計ddd的時候已經差不多約定好了的原項目的哪些包應該放在重構後的項目的哪個包下,以結果為驅動去重構,但是出了問題不好還原,所以這裏按照開發流程順序重構可以儘量地在重構期間不出紕漏。(但是還是先撿着目標整,比如先重構用户領域,那按照開發流程解構的時候就要先把包內和用户相關的實體等用户領域的相關歸屬放到domain.user下對應的包中以及interfaces用户接口層,比如原包下的model.dto.user => interfaces.dto.user;model.vo.UserVO和LoginUserVO => interfaces.vo.user下)。

===

用户領域

重構 model 包

重構 constant 包

其實也一個用户角色的常量類,直接拖到domain.user下就好了。

至於為什麼不和枚舉類一樣放到 domain.user.valueobject 下,是因為枚舉類裏是會有一些邏輯的,比如根據value獲取枚舉類的方法,而常量類就單純的常量而已,所以直接連帶着包拖過去單獨存在,區分度會更好一點。

重構數據訪問層(也就是mapper)

應用上了 - 依賴倒置原則 - ,在 domain.repository 中定義與數據庫交互的接口,然後下放到 infrastructure基礎設施層中寫相應的實現,領域內 提供接口,不實現邏輯。

又由於項目中使用的是 Mybatis-Plus 框架,可以讓接口直接繼承其提供的 IService 接口,接口的實現繼承 ServiceImlp 類,這樣不用寫任何邏輯,只需要定義一個接口類和一個該接口的實現類,藉助框架就可以直接擁有一批操作數據庫的方法,簡化開發。

流程就是:在domain包中定義一個repository 包,創建 UserRepository 接口並繼承框架提供的 IService 接口;然後在 infrastructure 包中定義一個repository 包,接着創建接口的實現類,繼承框架提供的 ServiceImpl<UserMapper, User>,然後implements UserRepository即可。

重構 Service(ps:最重要

先移動接口和實現類是有原因的,首先,重構之前,是controller接口層中調用了service的方法,也就是説原本的service是被controller調用的,現在新的應用服務層就是替代了原本的service,這樣接口層在我們重構service的時候會自動重構為引用我們新創建的應用服務層的業務代碼。

流程:將原包下 service.UserService 和 service.impl.UserServiceImpl 拖到新包下的 application.service 和 application.service.impl 下,然後選擇 UserService 按IDEA快捷鍵 shift + f6 進行全局重命名。-- 然後就會發現 未改動的 controller包中調用的原UserService方法的方法名都變成了新包下重命名後的方法(將原始服務接口的調用改為新應用服務接口的調用),減少了手動修改的代碼量。

複製過來之後將service接口和實現類中的方法中的繼承都刪除掉(在最底層的基礎設施層infrastructure中已經定義了與數據庫交互的接口UserRepository,在領域層直接注入調用即可實現對數據庫的操作),因為ddd中是要求上層調用下層,domain只需要乖乖等着上層的應用層調用即可。

儘量採用充血模型。

觀察service實現類中,只要不涉及調用了其他業務服務和領域的邏輯, 都可以下沉到領域層(領域服務或者實體中)。

應用服務要組合調用實體和領域服務。

牢記三個原則:

  1. 應用服務類要調用領域服務類,把領域服務類注入進來,把很多的調用改為調用領域服務;同時組合上領域類中實體的調用,要把應用服務類的邏輯下沉到領域服務和實體類中。
  2. 什麼情況下不能拆:這個方法中調用了其他領域的領域服務或者調用了應用服務,那就只能放到應用服務中,牢記上層只能調用下層 原則(也儘量不要跨層調用)。
  3. 要替controller層“擦屁股”,controller層需要調用的代碼以及複雜的邏輯都需要改寫到服務層(為之後的重構controller到用户接口層interfaces做鋪墊)。當然,如果發現寫在應用服務層中還可以下沉,那麼還可以繼續將這些邏輯下沉到領域服務中。

在領域服務層中,在該下沉的下沉完了、該重構的重構完了之後,還差給應用服務層兜底。應用服務層還需要實現數據庫增刪改查的方法,要麼繼承mybatis plus提供的 IService<> 接口(偷懶的方法,因為不符合ddd架構原則),要麼就...自己寫唄~~其實也就是把方法定義出來後直接 return userRepository.xxx() 就好了。

小技巧:如果在修改領域服務的時候發現有方法沒被調用(即在接口中顯示為灰色),那這個方法就可以被移除掉的。這樣可以避免同樣的一個方法既在實體裏出現了,又在service裏出現。

越往上層,都越儘可能地只用接口調用,邏輯下沉。

小練手:將controller層中的addUser方法也試着拆分一下。

 /**
     * 創建用户
     */
    @PostMapping("/add")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {
        ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);
        // 把所有參數取出來,再調用 UserService
        User user = new User();
        BeanUtils.copyProperties(userAddRequest, user);
        // 默認密碼
        final String DEFAULT_PASSWORD = "123456789";
        String encryptPassword = userApplicationService.getEncryptPassword(DEFAULT_PASSWORD);
        user.setUserPassword(encryptPassword);
        // 插入數據庫
        boolean result = userApplicationService.save(user);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(user.getId());
    }


~~以上是addUser方法,其中對象轉換的那一步可以聯想到在infrastructure基礎設施層中創建的 UserAssembler 用户轉換類,那麼想要調用它就需要基礎設施層的上一次domain領域層調用,也就是需要用户的領域服務類中調用轉換類其中的方法,所以先在用户領域服務類和接口中實現用户轉換的方法,再到領域層的上一層application應用服務層中調用領域服務實現用户轉換的方法,再到頂層的interfaces用户接口層中調用應用服務層的方法替換原邏輯,由此實現了邏輯下沉。(當然這裏可以直接調用,因為ddd四層結構中,每一層都可以調用最底層infrastructure基礎設施層的邏輯)~

上述描述錯誤!轉換類的包是定義在了頂層的用户接口層,犧牲掉一些合理性,直接在向上調用即可。 直接在controller方法中用轉換類方法替換原轉換邏輯(原轉換邏輯被拆分到了轉換類),屬於同級調用。

然後,controller層中的操作數據庫的代碼,應用服務層實現一層調用領域服務的接口,然後領域層中實現對基礎設施層中數據庫交互接口調用,實現邏輯下沉。儘量的保證越往上越精簡代碼越少。

現在又發現一種思路,還是比如這段addUser方法,可以在用户領域服務中實現所有controller方法的邏輯,然後在應用服務層中接口調用方法,最後在controller中用該方法替換原方法中的所有邏輯即可,這樣使得上層保留的代碼非常簡潔明瞭,而且也不用像剛剛上邊説的那樣,連着實現兩個用户轉換的方法只能使得部分邏輯下沉,這樣可以近乎全部的邏輯都下沉到領域服務中。

改完之後是這個效果
public BaseResponse addUser(@RequestBody UserAddRequest userAddRequest) {
return ResultUtils.success(userApplicationService.addUser(userAddRequest));
}
由此可見,僅需要一行調用即可。

如果説我們發現應用服務層既為controller提供服務了,也將邏輯下沉到領域服務中了,但是有些情況下還是會報錯。可能是因為有些應用服務,除了上層下層的調用,還會被同級的其他領域的應用服務調用。所以引申出,我們的應用服務層還有一層職責就是,為其他的應用服務提供調用(如果有需要的話)。

至此,第一個領域 用户領域 算是重構完畢了,接下來如法炮製,重構 圖片領域、空間領域,就可以了。


依舊,按照開發流程,從 model 層開始重構。

理論上從領域服務的接口都要刪掉對於框架接口的繼承,自己實現需要的增刪改查等基本方法,但是由於用户領域我們已經實操過一遍了,剩下的這兩個領域可以用這種偷懶的方法,這樣就可以既取得了便利,有學會了兩種重構方法。

在應用服務層解構原Picture的service時,發現有部分邏輯調用了其他領域的邏輯,所以按照嚴格意義上來講是不能夠將邏輯從應用服務層下沉到圖片的領域服務中的,因為下層不能調用上層。但是代碼過多地冗餘在上層不符合我們ddd重構項目的初衷,並且,如果按照拆解,需要重構完空間領域之後將圖片應用服務中相關邏輯分攤給空間並完成邏輯下沉,空間還有調用其他領域的邏輯,也需要同樣的做法,但是這時候,編碼過程中實踐大於理論就顯現出來了,如果真的嚴格按照ddd重構原則,剛才説的後者重構方法會花費更高的改造成本,那如果不嚴格符合原則直接將邏輯在圖片領域中下沉,就可以做到節約精力和成本的同時簡化開發流程,所以,實際生產活動中,還是以靈活為主。

所以,要時刻考慮成本,在不犧牲過多合理性的前提下,可以降本增效。

就像圖片應用服務的getPictureVOPage方法,其中調用了用户應用服務的方法,一定是不可以下沉邏輯的,和上邊講的為了降低成本而將邏輯下沉到圖片領域服務中不一樣,上邊涉及到的只是空間領域的校驗服務,下沉到圖片領域服務中其實是無傷大雅的,因為就是一個校驗邏輯而已,但是這裏涉及到的用户應用服務的邏輯是功能邏輯或者説是屬於業務邏輯了,一定是平級調用,所以為保證合理性,這裏不能下沉。

然後還是一樣的,等圖片的應用服務類邏輯都下沉處理好了之後,在領域服務類中處理,然後再利用小技巧點開領域服務接口類觀察呈灰色的方法(也就是沒有被使用的方法),直接從接口和實現類中刪掉。

對於需要轉換類的邏輯,因為轉換類包定義在了用户接口層,所以需要將沿途的接口調用都改成轉換之前的類,然後層層傳送到用户接口層之後,在用户接口層同級調用轉換類中的轉換方法,由此來保證下級不能調用上級的原則。

至此,圖片領域重構完畢(但是並沒有像用户領域一樣重構那麼細粒度)。


重構 空間模塊

在解構service時,對於空間分析實現類,它所有的方法實現都是通過調用空間應用服務類和圖片應用服務類來完成的,也就是通過其他兩個空間類調用它們對應的數據庫表達到空間數據分析的目的,所以空間分析類本身沒有數據庫表,因此不需要下放。所以複製空間和空間成員兩個接口及其實現類到領域層即可。

同樣的,還是不用去掉領域層和應用服務層中兩個模塊的繼承,這樣可以犧牲可接受範圍內少量的合理性,來達到提高開發效率的目的。

ps:複製到領域層後,impl下的實現類不要直接快捷鍵重命名,這樣不會自動重構實現類實現的接口類類名,要在類裏該名然後alt + enter重命名。

不光是調用了應用服務的方法不能下沉,調用了包含調用應用服務的方法的方法也不能下沉。所以還有一種方法,倒反天罡不處理~。不改就好了。

按照流程順序重構,最後是重構controller為用户接口層interfaces,第一步是將空間相關的controller類移動過去,第二步 是再assembler包中編寫對應的轉換類。最後查看邏輯是否需要下沉。

!!! 對於用户接口層中除了調用方法以外的業務邏輯,只需要在其對應的應用服務類中實現即可,如果在應用服務層中還可以下沉,那就放到其對應的領域服務中。


至此,三個模塊的重構都已經完成,剩下的核心處理就是 公共服務manager包 了。

首先,分析代碼中的成分。

比如:StpInterfaceImpl 類,它是Sa-Token權限校驗類,裏邊調用了多個領域的應用服務類,這就是一個公共服務。

由圖片中的指引,觀察到,manager.upload 包下的類,最多隻是用到了基礎設施層的代碼,不涉及領域層和應用服務層的邏輯,所以可以放到基礎設施層中。

當然也可以放到common裏而不必新建一個manager包,但是分開放更有區分度,因為common包下的類基本是沒有業務邏輯的,而manager還是有一些業務邏輯的,分開發區分度更好一些。

最後剩下的 manager.sharding 包,存放的是分庫分表邏輯。可以放到空間應用服務層中,但是歸根究底地説,分庫分表最根本還是針對圖片進行的,所以空間還是圖片就存在歧義,於是我們不如把該分庫分表包放到公共服務裏。

最後的最後,只剩下一些閒雜的東西了,按門分類地放到對應的層即可。

然後由於路徑的改變,需要全局搜索一下原項目根目錄名,接着替換為新項目路徑。

至此,整個雲圖庫項目的DDD領域驅動設計重構就完成了。

可以看一下結構對比:


和傳統開發目錄結構對比,業務實現更清晰,但是我大概率還是會選擇傳統開發結構。

因為首先ddd設計的話,前期的設計成本是要遠高於傳統結構的;其次分包太多也相對比較麻煩,而且強行多分一層,對於有些業務情況不一定就比直接寫在service中要簡單,如果是為了分而分就沒有必要了,一開始就已經指出了不要為了用ddd去用,而是要為了解決問題而去用。

通過對原項目進行整個的DDD重構,掌握DDD重構的思想和方法即可,在實際情況中是否要適用這種設計方式進行開發還是要針對實際問題靈活使用。

話又説回來,就算是用傳統的分層架構開發,難道就不能將ddd思想應用在其中嗎?答案當然是可以的,因為DDD是一種思想,甚至現在都沒有一個標準明確的架構規範,我們可以做到比如將controller層中的邏輯精簡到和ddd重構時的一樣,將邏輯在service中實現後調用,那麼這樣也算是DDD思想的一種靈活運用。所以不管是什麼設計、怎麼開發,靈活和邏輯清晰是最重要的。

重構完之後啓動項目,不會報編譯錯誤,但是前後端聯調會報錯(也就是開始登陸的時候)。------- 這個是因為我們的登錄態是存儲在 redis 中的,其中存儲了一個包名(完整帶目錄的),但是由於我們架構修改了其原位置,所以登錄態找不到對應的實體類的包名,所以將redis裏的數據清掉就可以了。

Add a new Comments

Some HTML is okay.