用最易懂的方式使用 Go 實現 DDD 的開發思想 —— 優雅的後端工程師系列(二) === 本篇主要是給出一些關於 DDD 架構的一些思考,使用 DDD 架構到底對於開發有什麼好處?一開始看到 DDD 架構的程式其實反而會覺得很難懂,各種跳來跳去,實現這個實現那個,又有各種依賴,但當真正理解了整個架構的寫法後就會發現,如果後續又有新的需求出現時,新增的程式相對於扁平化架構的侵入性小非常多,講人話就是扁平化架構更容易改這裡卻有奇怪的地方壞掉,而使用 DDD 架構的基本就只是新增一套東西,然後再把包含這個業務邏輯的結構體放在某個地方就完成了。 程式碼的侵入性哪裡來? --- 你可能會有點難理解是什麼東西可以讓程式的侵入性這麼高?如果需要新功能那麼重新寫一個函式不就好了嗎?剛開始我也有這樣的疑問,舉個簡單的例子,假設你用了一個結構體儲存商品資訊: ```go= type Product struct { ID int Name string Price float32 } ``` 你寫了一個函式,用來計算最後用戶要支付的金額,大概邏輯如下: ```go= if 用戶有優惠券 { totalPay := Product.Price * 0.8 return totalPay } ``` 你可能在很多個函式中都會需要這段邏輯,但當計算折扣的方式改變時,你就會發現根本不知道有哪裡要改,那麼假設現在使用 DDD 架構,我們將「計算折扣」這件事作為一個領域,而 Product 相關的方法依賴於 計算折扣領域,那麼只需要: ```go= totalPay := DiscountPolicy.ApplyDiscount(user, Product.Price) ``` 即便後續修改折扣邏輯,高層的依賴仍然不需要修改任何地方。 DDD 的核心思考 --- DDD 我認為更不傾向於程式開發,而在於理解業務。你應該先理解到底這個系統是做什麼的,或者核心功能要實現什麼,根據這個業務將其抽象成一個 Domain,然後將實際的業務邏輯封裝進這個 Domain 中,然後再實現這些業務邏輯。 還是用一個例子來說明,假設你正在開發一個申請餐廳預定系統,現在要做一個修改預定時間的功能,那麼你大概會這麼做: ```go= func (s *OrderService) ChangeOrderSchedule(ctx context.Context, user User, OrderID int) error { 透過 OrderID 獲取 Order 實體 o if user 不是 VIP { return 你沒錢你不能訂這個 } if o 原本就沒有預定 { return 沒預定不能改時間 } if o 預定的時間快到了 { return 預定的時間要到了不能修改 } if o 沒有被店家確認 { return 尚未確認不能修改 } 修改餐廳預定的時間、資訊......等 return nil } ``` 這只是一個簡單的邏輯範例,看起來並沒有什麼問題,但是當需求不斷增加,條件越來越多時也許 if 順序可能就會影響結果,漏掉一些分支狀況等等。這時候我們就可以將 Order 變成一個 Domain 實體: ```go= type Order struct { id int authUser user.User // 可以理解為 user 的層級,VIP VVIP 等等 authLevel auth.Level // 此預定的級別,不小於這個級別的客戶可以預定 startTime time.Time submitChangeScheduleTime time.Time // 什麼時候發起的請求 submitUser user.User // 誰發起的請求 approved bool // 店家是否確認 approvedBy user.User updatedTime time.Time updatedBy user.User } ``` 接下來的思路就是,盡可能的讓 service(或 application)的程式邏輯足夠簡單,盡量不要有業務邏輯在函式裡面,而是把業務邏輯放在 Domain 裡面,Domain 提供好現有的邏輯並且做好編排。 分析一下上面的業務邏輯,其實就是沒會員不能預約、沒會員不能改時間、快開始不能改時間、沒啟動不能改時間。我們用中文很好理解這些業務邏輯,而現在你的程式碼就要寫的盡可能貼近人話,而不是做一些資料庫的查詢修改。 比如你的產品經理告訴你:沒有 VIP 就不能預約,那麼你的程式不應該寫 ```if authUser.Level < authLevel```,應該要寫成 ```if authUser.HasPermission()``` 那麼現在可以想象一下,你的 Domain 的邏輯可能就是這樣: ```go= func (o *Order) NeedReschedule() bool { return o.submitChangeScheduleTime != nil } func (o *Order) UserAllow() bool { return o.submitUser.HasPermission(o.authLevel) } func (o *Order) RescheduleApproved() bool { if !o.approved { return false } return o.approvedBy.ID != o.submitUser.ID } func (o *Order) NearStartTime() bool { return o.submitRescheduleTime.Sub(o.startTime) <= 你定的間隔 } func (o *Order) Approve(u user.User) error { if o.NearStartTime() { return errors.New("接近開始時間不能確認") } // 其他店家確認邏輯 ...... ... } func (o *Order) Reschedule(order Order) error { if !o.NeedReschedule() { return errors.New("用戶沒有提升更改") } if !o.UserAllow() { return errors.New("沒有權限") } if !o.RescheduleApproved() { return errors.New("店家沒有通過或不能自己確認自己") } // 其他檢查,接下來進行修改 Order 資訊 } ``` 經過上面的準備之後,你的 service 層(或者 CQRS 概念中的 Command)就會變成這樣: ```go= func (h ChangeOrderScheduleHandler) Handle(ctx, cmd) error { return h.repository.Update(ctx, func(o Order) error { if err := o.Approve(cmd.User); err != nil { return err } if err := o.Reschedule(cmd.Order); err != nil { return err } return nil }) } ``` 我想上面的程式碼應該相當好理解且完全沒有耦合了,看到程式碼就可以知道,我要更新資料,然後要確認再修改,裡面甚至沒有寫到權限符不符合等等的問題,具體的業務邏輯在做什麼都不關心,我只知道我要做什麼步驟就可以完成這次操作。 以上就是整個 DDD 的核心思想,是不是覺得很神奇呢!同時也呼應了本系列第一篇文章所說的:接口應該設計成行為取向,而非新刪修查。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up