这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战
本文为译文,原文链接:www.calhoun.io/moving-towa…
这篇文章的目的是帮助说明一个应用程序如何随着时间发展的,这有助于我们可以理解有些更DDD能帮助到的问题。为了达到那个目的,我们准备看看一个相当琐碎的工程随着时间发展。这个工程不完整-这个样本代码不能编译通过,无法被一个编译器测试,并且甚至没有列出imports。它很简单意味着是一个可以跟着的一个例子。那就是说,如果有任何很容易看出来不对的地方,我会修改或者回答你的问题。
首先,我们讨论这个项目。想象一下你已经在工作了,并且你的老板要求你创建一个方式来通过GitHub的API来认证用户。更特别的是,你将获得用户的个人访问token,并且你需要去寻找这个用户包括它所有的组织。这样,您以后就可以根据他们所属的组织来限制他么的访问。
注意:我们使用访问token来简化例子。
听起来足够简单,所以你启动你的编辑器并且敲出一个github
的包来提供这个函数。
package github
type User struct {
ID string
Email string
OrgIDs []string
}
type Client struct {
Key string
}
func (c *Client) User(token string) (User, error) {
// ... interact with the github API, and return a user if they are in an org with the c.OrgID
}
注意:我没有真的使用了Github API-这是一个编出来的例子。
接下来你可以使用你的package
包,并且写一些中间件来保护我们的HTTP处理器。在这个中间件,你会从一个基础的auth的header头信息检索到一个用户的访问token,并且使用GitHub代码来查阅用户,校验查看他们是否是提供的组织中的,然后相应地授权或者拒绝访问。
package mw
func AuthMiddleware(client *github.Client, reqOrgID string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, ok := r.BasicAuth()
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err := client.User(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
permit := false
for _, orgID := range user.OrgIDs {
if orgId == reqOrgID {
permit = true
break
}
}
if !permit {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// user is authenticated, let them in
next.ServeHTTP(w, r)
})
}
你把这个展示给你的同事看,他们会关注缺失了测试。更具体地说,看起来没有一种方式可以校验这个AuthMiddleware
可以像前面说的那样生效。你说:“没问题,我使用接口就可以测试了”
package mw
type UserService interface {
User(token string) (github.User, error)
}
func AuthMiddleware(us UserService, reqOrgID string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, ok := r.BasicAuth()
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err := us.User(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
permit := false
for _, orgID := range user.OrgIDs {
if orgId == reqOrgID {
permit = true
break
}
}
if !permit {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// user is authenticated, let them in
next.ServeHTTP(w, r)
})
}
现在你可以使用一个mock的user service来测试这段代码。
package mock
type UserService struct {
UserFn func(token string) (github.User, error)
}
func (us *UserService) Authenticate(token string) (github.User, error) {
return us.UserFn(token)
}
有很多的方式来创建这个mock的user service,但是这是一种非常常见的方法来测试两种案例,包括认证是可以工作的和返回error的情况。
鉴权的中间件就可以测试了,释放了,生活看起来好极了。
然后悲剧发生了。你的CEO听说哥斯拉正在去旧金山的路上了,然后你的公司无法继续使用那种不确定性的GitHub了。如果他们整个办公室被炸毁了,然后没有工程师知道这个项目了。不,那行不通。完全无法接受。
幸运的是,还有一个替代公司叫做gitlab,看起来可以做很多GitHub相同的事情,但是他们有一个远程团队。那意味着哥斯拉永远无法消灭他们所有的工程师,对吧?
你公司的高层们看起来同意这种逻辑,他们开始做出改变。你的工作呢?你的任务是确保你写的所有鉴权代码可以在新的系统上面工作。
你花费了一些时间查阅了gitlab的API文档,然后好消息是它看起来整体策略依然可以工作。gitlab有个人的访问token和组织架构,你只需要重新实现客户端。中间件中的代码根本不需要做任何改变,因为你是一个小聪明,你使用了一个接口!你创建了gitlab client就搞定了...
package gitlab
type User struct {
ID string
Email string
OrgIDs []string
}
type Client struct {
Key string
}
func (c *Client) User(token string) (User, error) {
// ... interact with the gitlab API, and return a user if they are in an org with the c.OrgID
}
然后你把它接入到AuthMiddleware
,但是它无法工作了。
事实证明,甚至接口也会成为耦合的受害者。在这个case中,因为你的接口中的User
方法预期需要返回一个github.User
。
type UserService interface {
User(token string) (github.User, error)
^^^^^^^^^^^
}
你会怎么做呢?你的老板要求你明天就搞定!
所以,预知后事如何,请听下回分解。