关注【索引目录】服务号,更多精彩内容等你来探索!
在本文中,将解释什么是依赖注入,以及它如何解决 Go 语言构建应用程序时的一个关键问题。为了更具体一点,在使用测试驱动开发 (TDD) 构建跟踪器应用程序后端时遇到的一个问题。
当开始编写身份验证服务,特别是像GetUserByID和GetUserByEmail这样的函数时,问题出现了。为了测试服务逻辑,使用模拟数据库和模拟记录器编写了一些初始测试。然而,犯了一个常见的错误:将 UserService 与这些模拟函数紧密耦合。
这意味着,要将应用程序迁移到生产环境,必须返回并手动修改 UserService 及其所有功能,使其使用真实的数据库和记录器。这个过程既累人,又容易出错,而且一点也不好玩。解决方案是一种名为“依赖注入”(DI)的模式。
问题:紧密耦合的代码
让我们看看这种紧耦合的代码是什么样子的。想象一下一个UserService需要与数据库通信并记录消息。
如果没有 DI,您可能会像这样编写,其中服务创建自己的依赖项。
package main
import (
"fmt"
"log"
)
// A hard-coded database connection (for demonstration)
type PostgresDB struct {
// connection fields would go here
}
func (db *PostgresDB) GetUser(id int) (string, error) {
// In a real app, this would query the database
return fmt.Sprintf("user_%d", id), nil
}
// Our service that needs a database
type UserService struct {
db *PostgresDB // A direct dependency on a concrete type
}
// The constructor creates its own dependency
func NewUserService() *UserService {
// The problem is right here!
// We are creating the dependency inside the service.
// This makes it impossible to replace `PostgresDB` for tests or other environments.
return &UserService{
db: &PostgresDB{},
}
}
func (s *UserService) GetUser(id int) (string, error) {
log.Printf("Getting user %d", id)
return s.db.GetUser(id)
}
func main() {
userService := NewUserService()
user, err := userService.GetUser(101)
if err != nil {
log.Fatalf("Failed to get user: %v", err)
}
fmt.Println(user)
}
这种方法的问题很明显:
难以测试:如果没有运行 PostgreSQL 数据库,如何对 UserService 进行单元测试?你无法轻松地用 mock替换PostgresDB 。
不灵活:如果要切换到 MySQL 或使用其他日志库怎么办?您必须直接修改 UserService 代码。
解决方案:引入依赖注入
依赖注入意味着我们将“注入”(传入)服务所需的依赖项。无需服务创建其依赖项,而是我们将其传递给服务。
步骤 1:定义接口(“契约”)
首先,我们定义接口来描述服务需要什么,而不是如何实现。这就是契约。
// UserStorer is an interface that defines the operations we need for user data.
type UserStorer interface {
GetUser(id int) (string, error)
}
// Logger is an interface for logging messages.
type Logger interface {
Log(message string)
}
步骤 2:依赖接口,而不是具体类型
接下来我们重构UserService来依赖这些接口。
// UserService now depends on the interfaces, not the concrete structs.
type UserService struct {
store UserStorer
logger Logger
}
// The dependencies are "injected" as arguments to the constructor.
func NewUserService(s UserStorer, l Logger) *UserService {
return &UserService{
store: s,
logger: l,
}
}
func (s *UserService) GetUser(id int) (string, error) {
s.logger.Log(fmt.Sprintf("Getting user %d", id))
return s.store.GetUser(id)
}
我们的 UserService 现在与任何特定的数据库或记录器实现完全分离!
在 main() 函数中将所有内容连接在一起。
主函数(或初始化应用程序的任何位置)将成为“组合根”。在这里,您可以选择并创建具体的实现,并将其注入到您的服务中。
package main
import (
"fmt"
"log"
)
/*
--- Interfaces (Contracts) ---
*/
type UserStorer interface {
GetUser(id int) (string, error)
}
type Logger interface {
Log(message string)
}
/*
--- Production (Concrete) Implementations ---
*/
// PostgresDB is our real database implementation.
type PostgresDB struct { /* ... */ }
func (db *PostgresDB) GetUser(id int) (string, error) {
return fmt.Sprintf("user_%d from postgres", id), nil
}
func NewPostgresDB() *PostgresDB { return &PostgresDB{} }
// StandardLogger is our real logger implementation.
type StandardLogger struct{}
func (l *StandardLogger) Log(message string) {
log.Println(message)
}
func NewStandardLogger() *StandardLogger { return &StandardLogger{} }
/*
--- Service ---
*/
// UserService depends only on interfaces.
type UserService struct {
store UserStorer
logger Logger
}
func NewUserService(s UserStorer, l Logger) *UserService {
return &UserService{store: s, logger: l}
}
func (s *UserService) GetUser(id int) (string, error) {
s.logger.Log(fmt.Sprintf("Getting user %d", id))
return s.store.GetUser(id)
}
/*
--- Main (Composition Root) ---
*/
func main() {
// 1. Create our concrete dependencies for PRODUCTION.
db := NewPostgresDB()
logger := NewStandardLogger()
// 2. Inject them into our service.
userService := NewUserService(db, logger)
// 3. Use the service.
user, err := userService.GetUser(101)
if err != nil {
log.Fatalf("Failed to get user: %v", err)
}
fmt.Println(user) // Output: user_101 from postgres
}
回报:简单有效的测试✅
现在到了最精彩的部分。测试 UserService 逻辑非常简单。我们可以创建接口的模拟实现,使其行为完全符合我们的测试用例要求。
测试文件(main_test.go)可能如下所示:
package main
import (
"testing"
"fmt"
)
// MockUserStore is a mock implementation of UserStorer for testing.
type MockUserStore struct {
users map[int]string
}
func (m *MockUserStore) GetUser(id int) (string, error) {
user, ok := m.users[id]
if !ok {
return "", fmt.Errorf("user not found")
}
return user, nil
}
// MockLogger does nothing, perfect for silent testing.
type MockLogger struct {}
func (m *MockLogger) Log(message string) { /* do nothing */ }
// Test the UserService in isolation.
func TestUserService_GetUser(t *testing.T) {
// 1. Setup our MOCK dependencies.
mockStore := &MockUserStore{
users: map[int]string{
1: "Alice",
},
}
mockLogger := &MockLogger{}
// 2. Inject the mocks into our service.
userService := NewUserService(mockStore, mockLogger)
// 3. Run the test.
user, err := userService.GetUser(1)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if user != "Alice" {
t.Errorf("Expected user 'Alice', got '%s'", user)
}
}
我们刚刚测试了UserService逻辑,无需数据库或写入标准输出。它简洁、独立且快速。
结论:DI 是 Go 的核心模式
依赖注入并非添加一个复杂的框架。它是一种简单的模式,从外部赋予对象依赖项,而不是让对象在内部创建依赖项。
通过依赖接口并注入它们,您可以获得:
可测试性:轻松将真实实现替换为模拟实现。
灵活性:无需触及核心服务逻辑即可更改数据库或记录器的实现。
清晰度:NewUserService 的函数签名清楚地说明了其依赖关系。
对于大多数 Go 项目来说,这种手动的 DI 方法就足够了。它符合 Go 语言的语法,易于理解,而且非常有效。
关注【索引目录】服务号,更多精彩内容等你来探索!

