[翻译]在Golang中使用高阶函数实现依赖注入

原文:Dependency injection in Golang using higher order functions

你可以在github.com/steinfletcher/func-dependency-injection-go上找到完整的代码示例。该示例包含一个公开REST端口的http服务。

介绍

在这篇文章中,我们提出了一种在go中依赖注入的方法——使用高阶函数和闭包。

先看看下列返回用户属性的方法。

1
2
3
4
5
func GetUserProfile(id string) UserProfile {
rows, err := db.Query("SELECT ...")
...
return profileText
}

我们期望将处理用户数据的代码和访问数据库的代码分开。在这个例子中,我们希望通过提供对数据库访问的模拟,对主要业务层和其他业务逻辑进行单元测试。接下来分开这些,以至于每个方法都有一个独立的责任。

1
2
3
4
5
6
7
8
9
// 包含业务逻辑或匹配的核心业务层方法代码
func GetUserProfile(id string) User {
...
}

// 数据连接层方法
func SelectUserByID(id string) UserProfile {
...
}

我们也可以在其他域方法中重复使用方法SelectUserByID。我们需要一种将SelectUserByID注入到GetUserProfile的方法,以至于我们可以在测试中模拟数据访问,进行单元测试GetUserProfile方法。在Go中一种实现的方法是为函数定义一个类型别名。

类型别名

使GetUserProfile方法依赖一个抽象,意味着我们可以在测试中注入一个模拟的数据访问层。两种常用的方式分别是使用接口,或类型别名。类型别名很简单,不需要生成一个可以在这儿使用的新结构。接下来为这两个方法都声明别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type SelectUserByID func(id string) User

type GetUserProfile func(id string) UserProfile

func NewGetUserProfile(selectUser SelectUserByID) GetUserProfile {
return func(id string) string {
user := selectUser(id)
return user.ProfileText
}
}

func selectUser(id string) User {
...
return User{ProfileText: userRow.ProfileText}
}

SelectUserByID是一个传入用户ID,返回一个User的方法。我们没有定义它的实现。NewGetUserProfile是一个输入方法selectUser,然后返回一个可以被调用者使用的方法,的工厂方法。这种策略使用一个闭包,来提供一个内部方法可以被外部方法依赖的入口。闭包可以获得上下文定义的变量和常量。这被称为这些变量和常量的封闭。

可以像下面这样调用这些域函数。

1
2
3
4
// 在应用中的直接依赖
getUser := NewGetUserProfile(selectUser)

user := getUser("1234")

另一种方式

如果你熟悉其他类似Java的语言,这就像创建一个类,注入这个类的依赖到构造函数中,然后通过方法访问依赖。这种访问方式没有方法上的差异——可以将函数别名当作一个只有一个简单抽象方法(SAM)的接口。在Java中,可以使用构造函数来注入依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface DB {
User SelectUser(String id)
}

public class UserService {
private final DB db;

public UserService(DB db) { // 将依赖注入的构造函数
this.DB = db;
}

public UserProfile getUserProfile(String id) { // 访问方法
User user = this.DB.SelectUser(id);
...
return userProfile;
}
}

而在Go中使用高阶函数实现等价功能的方式如下:

1
2
3
4
5
6
7
8
9
10
11
type SelectUser func(id string) User

type GetUserProfile func(id string) UserProfile

func NewGetUserProfile(selectUser SelectUser) { // 注入依赖的工厂方法
return func(id string) UserProfile { // 访问方法
user := selectUser(id)
...
return userProfile
}
}

测试

现在可以通过模拟数据访问层来单元测试业务层方法。

1
2
3
4
5
6
7
8
9
10
func TestGetUserProfile(t *testing.T) {
selectUserMock := func(id string) User {
return User{name: "jan"}
}
getUser := NewGetUserProfile(selectUserMock)

user := getUser("12345")

assert.Equal(t, UserProfile{ID: "12345", Name: "jan"}, user)
}

你可以在github.com/steinfletcher/func-dependency-injection-go找到更完整的代码示例。该示例包含一个公开REST端口的http服务。

译者注:
本文翻译可能不够完美,如有错误请指出,我将改正。