依赖注入与控制反转:优化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编写的,但这里描述的原则同样适用于其他编程语言。

相关文章
|
4月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
4月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
17天前
|
存储 自然语言处理 算法
基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
本文探讨了在构建对话系统时如何通过一种内存高效算法降低大语言模型(LLM)的Token消耗和运营成本。传统方法中,随着对话深度增加,Token消耗呈指数级增长,导致成本上升。
86 7
基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
|
22天前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
42 10
|
3月前
|
人工智能 自然语言处理 前端开发
【2025.3.08更新】wordpress AI智能插件|自动生成SEO文章/图片/视频+长尾词优化 内置DeepSeek多模型支持与API扩展
Linkreate WordPress AI插件提供强大的自动化文章生成、SEO优化、关键词管理和内容采集功能。它能根据关键词自动生成高质量文章,支持多语言和批量生成,内置长尾关键词生成工具,并可定时自动发布文章。插件还集成了多种AI服务,支持前端AI客服窗口及媒体生成,帮助用户高效管理网站内容,提升SEO效果。
【2025.3.08更新】wordpress AI智能插件|自动生成SEO文章/图片/视频+长尾词优化 内置DeepSeek多模型支持与API扩展
|
3月前
|
数据可视化 JavaScript 前端开发
利用Postman和Apipost进行API测试的实践与优化-动态参数
在API测试中,Postman和Apipost是常用的工具。Postman内置变量功能有限,面对复杂场景时需编写JavaScript脚本,增加了维护成本。而Apipost提供丰富的内置变量、可视化动态值配置和低代码操作,支持生成真实随机数据,如邮箱、手机号等,显著提升测试效率和灵活性。对于复杂测试场景,Apipost是更好的选择,能有效降低开发与维护成本,提高测试工作的便捷性和可维护性。
|
4月前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
64 3
|
4月前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
4月前
|
SQL 安全 Java
阿里双十一背后的Go语言实践:百万QPS网关的设计与实现
解析阿里核心网关如何利用Go协程池、RingBuffer、零拷贝技术支撑亿级流量。 重点分享: ① 如何用gRPC拦截器实现熔断限流; ② Sync.Map在高并发读写中的取舍。
148 0
|
Go API Python
Go 装饰器模式在 API 服务程序中的使用
Golang 开发 API server   Go 语言是由谷歌主导并开源的编程语言,和 C 语言有不少相似之处,都强调执行效率,语言结构尽量简单,也都主要用来解决相对偏底层的问题。
1569 0

热门文章

最新文章

下一篇
oss创建bucket