包依赖与接口设计
讨论 Go 包循环依赖的处理方式,并用通知器示例说明接口与依赖注入。
包依赖与接口设计
循环依赖暴露了什么
a 导入 b,b 又导入 a,说明包的职责或依赖方向需要调整。处理方式取决于真正共享的内容:
- 依赖行为:在使用方定义小接口,由外部传入实现。
- 共享数据类型:把稳定、职责明确的类型放到独立包里。
- 只需要一个操作:传入回调函数即可。
Pixiu 里不是什么东西都能放进 model 和 common 里的,这点需要注意,希望最后不用重构。
用计费服务理解依赖注入
计费服务完成支付后需要通知用户,但不必知道通知是邮件还是短信。依赖注入就是由外部创建通知器,再交给计费服务。
使用方定义接口
services/billing/billing.go 只依赖 Notify 这一行为:
package billing
import "fmt"
// 1. 定义一个“通知器”接口,这是我们的“插槽标准”
// BillingService 不关心具体怎么发通知,它只需要一个能 Notify 的东西。
type Notifier interface {
Notify(userID int, message string) error
}
// 2. BillingService 结构体,它包含一个接口类型的字段
type Service struct {
notifier Notifier // 依赖的是抽象的接口,而不是具体的实现
}
// 3. 构造函数,接收一个满足 Notifier 接口的实例,并“注入”进来
func NewService(n Notifier) *Service {
return &Service{
notifier: n,
}
}
// PayInvoice 是核心业务逻辑
func (s *Service) PayInvoice(userID int, amount float64) error {
// ... 一些计费逻辑 ...
fmt.Printf("Processing invoice for user %d, amount %.2f\n", userID, amount)
// 4. 使用依赖(调用接口方法),它不知道具体是哪个实现在工作
message := fmt.Sprintf("Your invoice for $%.2f has been paid.", amount)
err := s.notifier.Notify(userID, message)
if err != nil {
return fmt.Errorf("failed to send notification: %w", err)
}
fmt.Println("Billing process completed successfully.")
return nil
}
通知器实现接口
Go 使用隐式接口实现;通知器只要具有相同的方法签名,就能传给计费服务,不需要导入 billing 包。
notifiers/email.go:
package notifiers
import "fmt"
// EmailNotifier 是一个具体的实现
type EmailNotifier struct {
// 可以有自己的字段,比如 SMTP 服务器地址等
AdminEmail string
}
// 实现 Notifier 接口的 Notify 方法
func (e EmailNotifier) Notify(userID int, message string) error {
// 实际的邮件发送逻辑
fmt.Printf("--- Sending EMAIL to user %d ---\n", userID)
fmt.Printf("Message: %s\n", message)
fmt.Printf("Admin copy sent to: %s\n", e.AdminEmail)
fmt.Println("------------------------------")
return nil
}
notifiers/sms.go:
package notifiers
import "fmt"
// SMSNotifier 是另一个具体的实现
type SMSNotifier struct {
APIToken string // 短信服务商的 token
}
// 同样实现 Notifier 接口的 Notify 方法
func (s SMSNotifier) Notify(userID int, message string) error {
// 实际的短信发送逻辑
fmt.Printf("--- Sending SMS to user %d ---\n", userID)
fmt.Printf("Message: %s (Token: %s)\n", message, s.APIToken)
fmt.Println("----------------------------")
return nil
}
这两个示例用打印代替实际发送,重点是依赖关系。
在入口组装
下面假设模块名为 project。切换通知方式只改变传入的实现,计费逻辑无需变化。
package main
import (
"fmt"
"project/services/billing"
"project/notifiers"
"log"
)
func main() {
// === 场景一:使用邮件通知 ===
fmt.Println("### Running with Email Notifier ###")
// 1. 创建一个具体的依赖实例 (邮件通知器)
emailNotifier := notifiers.EmailNotifier{AdminEmail: "[email protected]"}
// 2. 将依赖实例注入到 BillingService 的构造函数中
// 因为 EmailNotifier 实现了 Notify(...) 方法,所以它满足 billing.Notifier 接口,可以被传入
billingSvc1 := billing.NewService(emailNotifier)
// 3. 调用业务方法
if err := billingSvc1.PayInvoice(101, 99.95); err != nil {
log.Fatal(err)
}
fmt.Print("\n=====================================\n")
// === 场景二:切换到短信通知 ===
fmt.Println("### Running with SMS Notifier ###")
// 1. 创建另一个具体的依赖实例 (短信通知器)
smsNotifier := notifiers.SMSNotifier{APIToken: "abcdef123456"}
// 2. 将这个新的依赖实例注入
// 注意:我们只是改变了传入的零件,billing.NewService 和 billingSvc 本身的代码完全不用动!
billingSvc2 := billing.NewService(smsNotifier)
// 3. 再次调用业务方法,行为已经改变
if err := billingSvc2.PayInvoice(202, 49.50); err != nil {
log.Fatal(err)
}
}