依赖注入
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 45 课,共 107 课。
依赖注入是一种让结构体从外部接收依赖项,而不是在内部创建依赖项的技术。在 Go 中,接口让这种模式变得自然而强大。
你不在结构体中硬编码特定实现,而是接收一个接口。这样,你就可以替换实现,而不必更改结构体的代码:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
现在创建一个依赖接口而不是具体类型的结构体:
type OrderService struct {
notifier Notifier // 通过接口注入的依赖
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
OrderService 不知道也不关心它使用的是电子邮件还是 SMS。创建服务时,你可以注入依赖项:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email:已下单:Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS:已下单:Phone
}
这种方式能让你的代码更灵活,也更容易测试。测试时,你可以注入一个不会实际发送消息的模拟通知器。在生产环境中,你可以注入真实的实现。两种情况下,结构体都保持不变。
挑战
简单让我们构建一个日志系统,展示依赖注入的强大之处。你将创建一个服务,它可以将日志写入不同的目标位置,而服务本身并不知道,也不关心这些日志实际写到哪里。
你将把代码分成三个文件:
logger.go:定义一个Logger接口,该接口要求包含一个Log(message string) string方法。然后创建两种不同的日志记录器实现:ConsoleLogger,包含一个Prefix字段。它的Log方法返回[CONSOLE] [Prefix]: [message]FileLogger,包含一个Filename字段。它的Log方法返回[FILE:[Filename]] [message]
service.go:创建一个依赖于Logger接口(而不是具体类型)的AppService结构体。添加构造函数NewAppService,它接收一个Logger并返回一个指向AppService的指针。为AppService添加一个名为DoWork(task string) string的方法,该方法使用注入的日志记录器记录消息Processing: [task],并返回日志记录器返回的内容。main.go:从输入中读取配置,创建两种日志记录器,将它们分别注入不同的AppService实例,并使用提供的任务调用每个服务的DoWork。打印每次服务调用的结果。
输入如下:
- 第 1 行:控制台日志记录器前缀
- 第 2 行:文件日志记录器的文件名
- 第 3 行:要处理的任务
例如,给定 INFO、app.log 和 user authentication,输出应为:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authentication注意,AppService 并不知道日志是写入控制台还是文件。它只会对注入的日志记录器调用 Log。这种灵活性正是依赖注入的核心:同一份服务代码可以与完全不同的日志记录器实现配合使用。
亲手试试
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取控制台日志记录器的前缀
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// 读取文件日志记录器的文件名
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// 读取要处理的任务
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO:使用 prefix 创建 ConsoleLogger
// TODO:使用 filename 创建 FileLogger
// TODO:创建一个 AppService,并注入 ConsoleLogger
// TODO:再创建一个 AppService,并注入 FileLogger
// TODO:将 task 传给每个服务的 DoWork 方法,并打印结果
fmt.Println("result1")
fmt.Println("result2")
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器