今天想來分享高內聚 (High Cohesion) 與低耦合 (Loose Coupling) 的概念
定義:一個模組(或類別)裡的功能彼此高度相關、有一致性。
目標:讓每個模組專注做一件事,功能明確。
優點:易於理解、測試和維護。
定義:模組彼此之間依賴程度低,不會直接控制對方的行為。
目標:讓一個模組變更時,其他模組不用或很少跟著改變。
優點:彈性高,可替換性強,容易單元測試。
我們會希望程式架構朝著高內聚與低耦合的架構前進,但實際上在實作過程中,則是會出現三個版本。
高內聚,高耦合
低內聚,低耦合
高內聚,低耦合
現在我們用使用者註冊功能這個需求,用 Go 實作三個版本的範例,來幫助大家理解,這三個版本,應該會長成什麼樣子。
type UserService struct{}
func (s *UserService) Register(email, password string) error {
// 驗證 email
if !strings.Contains(email, "@") {
return errors.New("invalid email")
}
// 模擬儲存
fmt.Println("Saved:", email)
// 模擬寄送歡迎信
fmt.Println("Sent welcome email to:", email)
return nil
}
func main() {
service := &UserService{}
err := service.Register("test@example.com", "123456")
if err != nil {
fmt.Println("Register failed:", err)
} else {
fmt.Println("Register success!")
}
}
這個版本應該是每個軟體工程師剛開始接觸程式時會寫的版本,或是 PoC 版本為了實現功能的可行性。
業務流程集中在 UserService,邏輯一目了然(高內聚)
初期開發快速直觀,容易看懂
不需要額外抽象、interface,程式碼少
直接綁定底層技術(DB、SMTP),導致耦合高
單元測試會比較複雜
變更其中某一個流程(如改寄信服務)時要改核心邏輯
無法重用或替換元件
type Validator struct{}
func (v *Validator) Validate(email string) error {
if !strings.Contains(email, "@") {
return errors.New("invalid email")
}
return nil
}
type DBWriter struct{}
func (d *DBWriter) Save(email, password string) error {
// 模擬儲存
fmt.Println("Saved:", email)
return nil
}
type EmailNotifier struct{}
func (n *EmailNotifier) Send(email string) error {
// 模擬寄送歡迎信
fmt.Println("Sent welcome email to:", email)
return nil
}
// 外部流程直接串接各個元件
func RegisterUserFlow(email, password string) error {
validator := &Validator{}
dbWriter := &DBWriter{}
notifier := &EmailNotifier{}
if err := validator.Validate(email); err != nil {
return err
}
if err := dbWriter.Save(email, password); err != nil {
return err
}
return notifier.Send(email)
}
func main() {
err := RegisterUserFlow("test@example.com", "123456")
if err != nil {
fmt.Println("Register failed:", err)
} else {
fmt.Println("Register success!")
}
}
這個版本比上一個版本更進階,我們試圖想要減少功能的耦合性,但卻可能造成業務邏輯的分散。
各模組單一責任,容易替換與測試(低耦合)
無硬綁定底層邏輯,可抽換不同實作(如 mock / stub)
方便獨立維護與重複使用元件
業務流程邏輯分散在 main() 或 controller(低內聚)
沒有中心協調者,不利於維護流程變化
每次要新增流程(如 log、驗證碼)都要改外部流程呼叫
難以重用整體註冊邏輯
type EmailSender interface {
Send(email string) error
}
type UserRepo interface {
Save(email, password string) error
}
type Validator interface {
Validate(email string) error
}
// EmailSender 實作
type SimulateEmailSender struct{}
func (s *SimulateEmailSender) Send(email string) error {
fmt.Println("Send welcome email to:", email)
return nil
}
// UserRepo 實作
type SimulateUserRepo struct{}
func (r *SimulateUserRepo) Save(email, password string) error {
fmt.Println("Saved user to DB:", email)
return nil
}
// Validator 實作
type SimpleValidator struct{}
func (v *SimpleValidator) Validate(email string) error {
if !strings.Contains(email, "@") {
return errors.New("invalid email format")
}
return nil
}
type UserService struct {
validator Validator
repo UserRepo
sender EmailSender
}
func (s *UserService) Register(email, password string) error {
if err := s.validator.Validate(email); err != nil {
return err
}
if err := s.repo.Save(email, password); err != nil {
return err
}
return s.sender.Send(email)
}
func main() {
service := &UserService{
validator: &SimpleValidator{},
repo: &SimulateUserRepo{},
sender: &SimulateEmailSender{},
}
err := service.Register("test@example.com", "123456")
if err != nil {
fmt.Println("Register failed:", err)
} else {
fmt.Println("Register success!")
}
}
這個版本通常已經是維護到中大型專案時,很常見的程式架構。
業務邏輯集中在 UserService(高內聚)
所有依賴透過 interface 傳入,方便替換 / 測試(低耦合)
新增流程只需修改 UserService,維護成本低
測試時可 mock interface,單元測試容易寫
更符合 Clean Architecture、SOLID 原則
初期設定較多,需要定義多個 interface 和實作
對小型專案來說略顯複雜(但對中大型專案是必要的)
這篇文章摘自個人每週電子報的技術分享部分,歡迎關注與交流,也希望能帶給大家一些啟發。