到目前為止,我們的架構在處理 CRUD(建立、讀取、更新、刪除)類型的功能時表現出色。
但真實世界的業務遠比這複雜。如果需求變更為:「使用者註冊成功後,需要同時發送一封歡迎郵件、建立一個試用訂閱、並記錄一條審計日誌」,我們該怎麼辦?
如果將所有邏輯都塞進 Register
這個 UseCase 中,它會迅速變得臃腫、難以維護,且與郵件、計費、日誌等毫不相關的領域緊緊地耦合在一起。這就是「重構」的信號。
Execute
方法有上百行程式碼,一個 UseCase
結構體注入了七八個不同的依賴。這違背了「單一職責原則」。UseCase
開始需要 import
billing
或 notification
的套件。這表示不同業務領域之間的邊界開始變得模糊。if-else
,試圖處理太多不同的場景。Register
UseCase// 重構前
func (u *UseCase) Execute(ctx context.Context, input Input) (*Output, *domain.GPError) {
// 1. 檢查使用者是否存在 (userRepo)
// 2. 建立使用者 (userRepo)
// 3. 發送歡迎郵件 (notificationSvc) // 新增的依賴
// 4. 建立試用訂閱 (billingSvc) // 又一個新依賴
// 5. 記錄審計日誌 (auditSvc) // 再一個新依賴
// 6. 回傳結果
return nil
}
要解耦這些職責,一個極其強大的模式是領域事件。其核心思想是:
當一個領域中發生了重要的事(領域事件),系統的其他部分可以「訂閱」並「回應」這個事件,而無需事件的發布者知道它們的存在。
這是一種基於「發布-訂閱」模式的非同步解耦機制。
UserRegistered
事件,包含必要的資訊(如使用者 ID、Email)。dispatcher
介面,並提供一個實作,負責將事件傳遞給所有訂閱者。UserRegistered
事件的功能(如發送郵件、建立訂閱、記錄日誌),建立一個獨立的 Listener。// internal/domain/event_payload.go
package domain
const (
EventUserRegistered EventName = "UserRegistered"
)
type EventUserRegisteredPayload struct {
UserID int
Email string
}
// internal/domain/event.go
type Listener interface {
Handle(ctx context.Context, payload any) error
}
type Service struct {
listeners map[domain.EventName][]domain.Listener
logger logger.Logger
}
func NewService(logger logger.Logger) *Service {
return &Service{
listeners: make(map[domain.EventName][]domain.Listener),
logger: logger,
}
}
// dispatch 將事件傳遞給所有訂閱者
func (s *Service) dispatch(ctx context.Context, name domain.EventName, payload any) error{
listeners, ok := s.listeners[name]
if !ok {
return fmt.Errorf("no listeners for event: %s", name)
}
for _, listener := range listeners {
if err := listener.Handle(ctx, payload); err != nil {
return err
}
}
return nil
}
// RegisterListener 註冊事件監聽器
func (s *Service) RegisterListener(name domain.EventName, listener domain.Listener) {
s.listeners[name] = append(s.listeners[name], listener)
}
// DispatchUserRegistered 發布 UserRegistered 事件
// 這裏不是讓外部傳入 domain.EventUserRegisteredPayload,而是傳入必要的參數
// 這樣可以保證 event name 與 payload 的一致性
// 這裡可以根據需要同步與非同步 來決定是不是要回傳 error
func (s *Service) DispatchUserRegistered(ctx context.Context, userID int, email string) {
go func() {
err := s.dispatch(ctx, domain.EventUserRegistered, domain.EventUserRegisteredPayload{
UserID: userID,
Email: email,
})
if err != nil {
s.logger.With(ctx).Error(ctx, "failed to dispatch user registered event", err)
}
}()
}
type EmailService interface {
SendEmail(email string, body any) error
}
type WelcomeEmail struct {
emailService EmailService
}
func NewWelcomeEmail(emailService EmailService) *WelcomeEmail {
return &WelcomeEmail{emailService: emailService}
}
// Handle 實作 domain.Listener 介面
func (w *WelcomeEmail) Handle(ctx context.Context, event any) error {
userRegisteredEvent, ok := event.(domain.EventUserRegisteredPayload)
if !ok {
return fmt.Errorf("invalid event type")
}
// Send welcome email
return w.emailService.SendEmail(userRegisteredEvent.Email, "Welcome!")
}
// internal/application/service.go
dispatcherService := dispatcher.NewService(app.Logger)
dispatcherService.RegisterListener(domain.EventUserRegistered, listener.NewWelcomeEmail(emailService))
Register
UseCasetype dispatcher interface {
DispatchUserRegistered(ctx context.Context, userID int, email string)
}
type UseCase struct {
...
dispatcher dispatcher // 新增的依賴
}
func (u *UseCase) Execute(ctx context.Context, input Input) (*Output, *domain.GPError) {
// 1. 檢查使用者是否存在 (userRepo)
// 2. 建立使用者 (userRepo)
// 發布領域事件
u.dispatcher.DispatchUserRegistered(ctx, userID, input.email)
// 6. 回傳結果
return nil
}
我們可以看到,Register
UseCase 現在只專注於「註冊使用者」這一件事。當使用者成功註冊後,它發布了一個 UserRegistered
事件。
其他需要回應這個事件的功能(如發送郵件、建立訂閱、記錄日誌)都被移到了獨立的 Listener 中,這些 Listener 完全不知道 Register
UseCase 的存在。
另外一個好處是,當你提供使用者多種註冊方式的時候(例如 Email 註冊、Google OAuth 註冊),你只需要在這些不同的 UseCase 中發布相同的 UserRegistered
事件,而不需要重複實作發送郵件或建立訂閱的邏輯。
User
領域完全不知道 Email
或 Billing
領域的存在。SlackNotificationListener
並註冊它即可,完全不需要修改任何 Register
的程式碼!當業務邏輯變得複雜時,使用領域事件是一種強大且靈活的解耦方式。它允許我們將不同的業務職責分離到獨立的模組中,提升系統的可維護性和擴展性。
然而,這種模式也帶來了一些挑戰,如非同步處理和調試困難,需要在設計時加以考慮。總的來說,領域事件是應對複雜業務邏輯的一個有效工具,值得在實際開發中嘗試和應用。
詳細的程式碼,請參考 Github 這一個 commit。