依赖注入与控制反转:优化Go语言REST API客户端

简介: 依赖注入与控制反转:优化Go语言REST API客户端

在这篇文章中,我将探讨依赖注入(DI)和控制反转(IoC)是什么,以及它们的重要性。作为示例,我将使用Monibot的REST API客户端。让我们开始吧:


一个简单的客户端实现


我们从一个简单的客户端实现开始,允许调用者访问Monibot的REST API,具体来说,是为了发送指标值。客户端的实现可能如下所示:


package monibot
type Client struct {
}
func NewClient() *Client {
    return &Client{}
}
func (c *Client) PostMetricValue(value int) {
    body := fmt.Sprintf("value=%d", value)
    http.Post("https://monibot.io/api/metric", []byte(body))
}


这里有一个客户端,提供了PostMetricValue方法,该方法用于将指标值上传到Monibot。我们的库的用户可能像这样使用它:


import "monibot"
func main() {
    // 初始化API客户端
    client := monibot.NewClient()
    // 发送指标值
    client.PostMetricValue(42)
}

依赖注入


现在假设我们想对客户端进行单元测试。当所有HTTP发送代码都是硬编码的时候,我们如何测试客户端呢?对于每次测试运行,我们都需要一个“真实”的HTTP服务器来回答我们发送给它的所有请求。不可取!我们可以做得更好:让我们将HTTP处理作为“依赖”;让我们发明一个 Transport 接口:


package monibot
// Transport传输请求。
type Transport interface {
    Post(url string, body []byte)
}


让我们再发明一个具体的使用HTTP作为通信协议的Transport


package monibot
// HTTPTransport是一个使用HTTP协议传输请求的Transport。
type HTTPTransport struct {
}
func (t HTTPTransport) Post(url string, data []byte) {
    http.Post(url, data)
}


然后让我们重写客户端,使其“依赖”于一个Transport 接口:


package monibot
type Client struct {
    transport Transport
}
func NewClient(transport Transport) *Client {
    return &Client{transport}
}
func (c *Client) PostMetricValue(value int) {
    body := fmt.Sprintf("value=%d", value)
    c.transport.Post("https://monibot.io/api/metric", []byte(body))
}


现在,客户端将请求转发到它的Transport依赖。当创建客户端时,transport(客户端的依赖项)被“注入”到客户端中。调用者可以这样初始化一个客户端:


import "monibot"
func main() {
    // 初始化API客户端
    var transport monibot.HTTPTransport
    client := monibot.NewClient(transport)
    // 发送指标值
    client.PostMetricValue(42)
}


单元测试


现在我们可以编写一个使用“伪造”Transport的单元测试:


// TestPostMetricValue确保客户端向REST API发送正确的POST请求。
func TestPostMetricValue(t *testing.T) {
    transport := &fakeTransport{}
    client := NewClient(transport)
    client.PostMetricValue(42)
    if len(transport.calls) != 1 {
        t.Fatal("期望1次传输调用,但是是%d次", len(transport.calls))
    }
    if transport.calls[0] != "POST https://monibot.io/api/metric, body=\\"value=42\\"" {
        t.Fatal("错误的传输调用 %q", transport.calls[0])
    }
}
// 伪造的Transport是单元测试中使用的Transport。
type fakeTransport struct {
    calls []string
}
func (f *fakeTransport) Post(url string, body []byte) {
    f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}


添加更多的Transport函数


现在假设我们库的其他部分,也使用了Transport功能,需要比POST更多的HTTP方法。对于它们,我们必须扩展我们的Transport接口:


package monibot
// Transport传输请求。
type Transport interface {
    Get(url string) []byte     // 添加,因为health-monitor需要
    Post(url string, body []byte)
    Delete(url string)         // 添加,因为resource-monitor需要
}


现在我们有一个问题。编译器抱怨我们的fakeTransport不再满足Transport接口。所以让我们通过添加缺失的函数来解决它:


// 伪造的Transport是单元测试中使用的Transport。
type fakeTransport struct {
    calls []string
}
func (f *fakeTransport) Get(url string) []byte {
    panic("不使用")
}
func (f *fakeTransport) Post(url string, body []byte) {
    f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}
func (f *fakeTransport) Delete(url string) {
    panic("不使用")
}


我们做了什么?由于在单元测试中我们不需要新的Get()Delete()函数,如果它们被调用,我们就抛出异常。这里有一个问题:每次在Transport中添加新函数时,我们都会破坏现有的fakeTransport实现。对于大型代码库来说,这将导致维护噩梦。我们能做得更好吗?


控制反转


问题在于我们的客户端(和相应的单元测试)依赖于一个它们不能控制的类型。在这种情况下,它是Transport接口。为了解决这个问题,让我们通过引入一个未导出的接口,该接口仅声明了我们的客户端所需的内容,来反转控制:


package monibot
// clientTransport传输Client的请求。
type clientTransport interface {
    Post(url string, body []byte)
}
type Client struct {
    transport clientTransport
}
func NewClient(transport clientTransport) *Client {
    return &Client{transport}
}
func (c *Client) PostMetricValue(value int) {
    body := fmt.Sprintf("value=%d", value)
    c.transport.Post("https://monibot.io/api/metric", []byte(body))
}


现在让我们将我们的单元测试更改为使用假的clientTransport


// TestPostMetricValue确保客户端向REST API发送正确的POST请求。
func TestPostMetricValue(t *testing.T) {
    transport := &fakeTransport{}
    client := NewClient(transport)
    client.PostMetricValue(42)
    if len(f.calls) != 1 {
        t.Fatal("期望1次传输调用,但是是%d次", len(f.calls))
    }
    if f.calls[0] != "POST https://monibot.io/api/metric, body=\\"value=42\\"" {
        t.Fatal("错误的传输调用 %q", f.calls[0])
    }
}
// 伪造的Transport是在单元测试中使用的clientTransport。
type fakeTransport struct {
    calls []string
}
func (f *fakeTransport) Post(url string, body []byte) {
    f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}


由于Go的隐式接口实现(如果愿意,可以称之为'鸭子类型'),我们库的用户什么也不需要改变:


import "monibot"
func main() {
    // 初始化API客户端
    var transport monibot.HTTPTransport
    client := monibot.NewClient(transport)
    // 发送指标值
    client.PostMetricValue(42)
}


重新审视Transport


如果我们使IoC成为规范(正如我们应该做的那样),就不再需要导出Transport接口了。为什么呢?因为如果消费者需要一个接口,让他们在自己的作用域中定义它,就像我们对'clientTransport'做的那样。


不要导出接口。导出具体实现。如果消费者需要接口,让他们在自己的作用域中定义。


总结


在这篇文章中,我展示了如何以及为什么在Go中使用DI和IoC。正确使用DI/IoC可以导致更易于测试和维护的代码,特别是在代码库不断增长时。虽然代码示例是用Go编写的,但这里描述的原则同样适用于其他编程语言。

目录
打赏
0
2
2
0
75
分享
相关文章
|
6天前
|
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
34 14
|
21天前
|
内网监控系统之 Go 语言布隆过滤器算法深度剖析
在数字化时代,内网监控系统对企业和组织的信息安全至关重要。布隆过滤器(Bloom Filter)作为一种高效的数据结构,能够快速判断元素是否存在于集合中,适用于内网监控中的恶意IP和违规域名筛选。本文介绍其原理、优势及Go语言实现,提升系统性能与响应速度,保障信息安全。
27 5
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
44 14
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
88 11
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
16 0
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
44 9
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
93 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
2月前
|
go语言中数组和切片
go语言中数组和切片
47 7
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等