Go语言必知必会100问题-05 接口污染

接口污染

在Go语言中,接口是我们设计和编写代码的基石。然而,像很多概念一样,滥用它是不好的。接口污染是指用不必要的抽象来编写代码(刻意使用接口),使得代码更难以理解。这是具有不同习惯,特别是有其它语言开发经验的人会犯的一个常见错误。在深入讨论接口污染之前,让我们重新梳理一下Go语言的接口,然后分析何时使用接口以及在什么时候使用会存在污染问题。

接口

接口约定了对象的行为方法,用于创建多个对象可以实现的通用抽象,也就是说接口规范了对象的通用方法。Go语言中接口有点特别,不像其它语言通过类似于implements关键字显示的标记对象X实现了接口Y, 它是隐式实现的。

接口如此灵活强大的原因是什么呢?为了搞清楚这个问题,我们从标准库中选两个广泛使用的接口: io.Reader 和 io.Writer 进行举例说明。

io包为I/O操作提供了抽象,I/O有读写两类操作。如下图所示,io.Reader是从数据源读取数据接口, io.Writer是将数据写入目标接口。

在这里插入图片描述

io.Reader接口包含一个Read方法。如果一个结构体要实现io.Reader接口,则需要实现下面的Read方法,该方法需要一个字节切片作为入参,会将从数据源读取的数据填充到入参切片中,同时返回读取的字节数和错误信息。

type Reader interface {Read(p []byte) (n int, err error)
}

io.Writer接口包含一个Write方法。 如果一个结构体要实现io.Writer接口,则需要实现下面的Write方法,该方法也需要一个字节切片作为入参,会将入参切片中的数据写入到目标中,并返回写入的字节数和错误信息。

type Writer interface {Write(p []byte) (n int, err error)
}

因此,这两个接口都提供了对基本操作的抽象:

  • io.Reader 从源读取数据
  • iO.Writer 将数据写入到目标中

在编程时使用这两个接口合理性在什么地方呢?创建这些抽象意义在哪里呢?下面通过一个例子进行说明。假设我们需要实现将一个文件内容复制到另一个文件中的函数,我们可以创建一个特定的函数,将两个 *os.File作为输入, 或者可以选择使用io.Reader和io.Writer接口创建一个更通用的函数。

func copySourceToDest(source io.Reader, dest io.Writer) error {// ...
}

copySourceToDest函数可以使用*os.File作为入参(因为*os.File实现了io.Reader和io.Writer),也可以使用任何其他实现了这些接口的类型。例如,我们可以创建自己的io.Writer来将数据写入到数据库中,并且可以不用修改copySourceToDest代码。这样增加了函数的通用性,因此,上述函数是可重用的。

使用接口除了使函数更有通用性,还使得为这个函数编写单元测试更容易,因为我们不必写文件,可以使用标准库中strings包和bytes包提供的功能实现测试。下面程序中source变量是*strings.Buffer类型,dest变量是*bytes.Buffer类型,我们可以在不创建任何文件的情况下测试copySourceToDest的行为。

func TestCopySourceToDest(t *testing.T) {const input = "foo"source := strings.NewReader(input)dest := bytes.NewBuffer(make([]byte, 0))err := copySourceToDest(source, dest)if err != nil {t.FailNow()}got := dest.String()if got != input {t.Errorf("expected: %s, got: %s", input, got)}
}

在设计接口时,不要忘了接口的粒度(接口中包含多少方法), Go语言中有一句名言描述了接口粒度问题:

接口越大,抽象越弱

向接口中添加方法会降低它的可重用性。io.Reader和io.Writer具有强大的抽象,因为它们都包含1个方法,不能再变得更抽象了。可以组合细粒度的接口来创建更高级别的抽象。像下面的ReadWriter接口组合了Reader和Writer接口,兼有读取和写入功能。

type ReadWriter interface {ReaderWriter
}

NOTE:正如爱因斯坦所说,“一切事情应该力求简单,不过不能过于简单”。应用到接口上,表示找到接口最佳粒度不一定是一个简单的事情。

什么时候使用接口

编写Go程序的时候,在什么情况下该创建接口呢?本文将深入研究三个具体的场景,在这些场景中,可以看到使用接口可以带给我们更多的收益。注意,对于使用接口的场景,本文没法全部列举完,因为每个案例都依赖于上下文。虽然没法全部列举,但本文列举的三个场景将给我们在什么情况应该使用接口提供一个指引。

  • 共同行为
  • 解耦
  • 限制行为

第一个讨论的场景是在多种类型实现共同行为时使用接口。这种场景下,将共同行为抽取到接口中。如果我们查看标准库,可以找到许多此类场景的示例。例如,可以通过实现排序接口的定义的方法对集合元素进行排序。

  • Len方法,获取集合中元素的数量
  • Less方法,判断一个元素是否在另一个元素之前
  • Swap方法,将两个元素互换位置

因此,在sort包中定义了如下接口:

type Interface interface {Len() intLess(i, j int) boolSwap(i, j int)
}

该接口具有强大的复用性,因为它支持对任何基于索引的集合进行排序。在整个sort包中,可以找到很多种实现。例如,具体到某种类型,在某个时候当我们计算出了集合中元素的个数之后,我们需要对其进行排序,我们是否一定对实现类型感兴趣?采用的是什么排序算法,是归并排序还是快速排序?在很多情况下,作为调用方并不在乎。因此,排序行为可以被抽象化,我们可以依赖于sort.Interface.

找到合适的抽象来分解操作也会带来很多好处,例如,sort包提供了同样依赖于sort.Interface的工具函数,像检查一个集合是否已经是有序的。

func IsSorted(data Interface) bool {n := data.Len()for i := n - 1; i > 0; i-- {if data.Less(i, i-1) {return false}}return true
}

第二讨论的场景是对我们的代码实现进行解耦。如果我们依赖抽象而不是具体的实现,那么实现本身就可以被另一个实现替换,甚至不用更改当前的代码。这就是里氏替换原则(SOLID中的L)。 此外,解耦可以带来单元测试的便利性。假设我们必须实现一个CreateNewCustomer方法来创建一个新客户并保存它的信息,我们可以直接依赖具体的实现(比如mysql.Store结构), 代码如下。

type CustomerService struct {store mysql.Store
}func (cs CustomerService) CreateNewCustomer(id string) error {customer := Customer{id: id}return cs.store.StoreCustomer(customer)
}

现在,如果我们要对这个函数进行单元测试,由于CustomerService依赖于实际实现(MySQL)来存储客户信息。我们需要先启动MySQL数据库,才能对其进行测试(除非使用诸如go-sqlmock之类的替代方法)。尽管集成测试很有帮助,但它并不总是我们想要的。为了使得代码有更大的灵活性,应该将CustomerService与实际实现分离,可以通过如下接口完成:

type customerStorer interface {StoreCustomer(Customer) error
}type CustomerService struct {storer customerStorer
}func (cs CustomerService) CreateNewCustomer(id string) error {customer := Customer{id: id}return cs.storer.StoreCustomer(customer)
}

上述新版本存储客户信息是通过接口完成的,我们现在可以灵活地对其进行单元测试:

  • 采用集成测试对其具体实现进行测试
  • 通过mock(模拟接口的行为)进行测试
  • 联合前面两种进行测试

第三个讨论的场景是通过接口限制特定的行为,看起来有点违反直觉,可以结合下面的例子进行理解。假设我们已经实现了一个自定义配置包来处理动态配置,该包中定义了一个IntConfig结构体,用于存储int配置信息,该结构体对外暴露了Get和Set两个方法.

type IntConfig struct {// ...
}func (c *IntConfig) Get() int {// Retrieve configuration
}func (c *IntConfig) Set(value int) {// Update configuration
}

现在,假设我们获取到一个IntConfig对象,它包含一些特定的配置,例如阈值设定。但是,在我们的代码中,只对读取配置感兴趣,并且希望不要对其进行修改操作。如果不想修改上面的配置包中的代码,怎么限制执行这个配置是只读的呢?可以创建一个将行为限制为仅读取配置值的抽象(即接口)。

type intConfigGetter interface {Get() int
}

然后,在代码中,可以依赖 intConfigGetter 而不是具体的实现编码。配置getter被注入到NewFoo工厂方法中,这样做甚至能够做到不会影响使用这个函数的客户端,仍然可以传递一个IntConfig对象给NewFoo,因为IntConfig实现了接口intConfigGetter,并且能够实现在Bar方法中只能读取不能修改配置信息的目的。

type Foo struct {threshold intConfigGetter
}func NewFoo(threshold intConfigGetter) Foo {return Foo{threshold: threshold}
}func (f Foo) Bar()  {threshold := f.threshold.Get()// ...
}

通过上面的例子可以看到,出于各种原因,我们可以使用接口来限制对象的特定行为,像上面强制设置为只读语义。

接口污染

有其他语言经验的人,像C#或Java背景的人,在具体类型之前创建接口对他们来说是很自然的。然而,在Go项目中这是在过度使用接口,不是推荐做法。

正如我们所讨论的,接口是用来创建抽象的。当在编码中遇到抽象时,记住一句话“应该发现抽象,而不是创建抽象”,这是什么意思呢? 这句话想表达的意思是如果没有直接的原因,我们不应该首先在代码中创建抽象,不应该使用接口进行设计,而是等待具体的需求。也就是说,我们应该在需要时创建接口,而不是在我们预见到可能需要它时就创建。

过度使用接口,会产生什么问题呢?答案是它使代码流更加复杂。添加无用的间接层不会带来任何价值:创建了一个没有用的抽象,使代码更难阅读和理解。如果没有充分的理由添加接口并且不清楚接口如何使代码变得更好,我们应该主动对使用接口产生质疑,为什么不直接调用具体实现(非接口)呢?

NOTE:注意通过接口调用方法时的性能开销,需要在哈希表数据结构中查找到实际指向的具体类型,然而,这在很多情况下不是什么问题,因为这种开销很小。

总结,在编码的过程中使用接口应该谨慎,应该带着发现抽象,而不是创建抽象的目的。对于软件开发人员来说,根据当前情况猜测以后可能有什么需求,来构建完美的抽象,过度设计代码是很常见的,应该避免这样做,因为在大多数情况下,会用不必要的抽象污染当前的代码。使其阅读起来更加复杂。我们不要试图通过抽象解决所有问题,而是解决现在必须解决的问题。最后但同样重要的是,如果不清楚接口如何使代码变得更好,我们可能应该考虑删除它以使我们的代码更简单。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/704700.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rust-windows安装环境

文章目录 前言一、Using rustup (Recommended)二、配置vscode解决办法:在终端依次运行如下两条指令: 总结 前言 Rust学习系列,之前介绍了macOS环境下的rust安装方式macOS rust安装。这篇学习windows的rust安装方式。 提示:以下是…

【STM32备忘录】【STM32WB系列的BLE低功耗蓝牙】一、测试广播配置搜不到信号的注意事项

文章目录 一、预备知识:二、准备工具:三、FUS和无线协议栈更新流程四、广播例程测试五、DEBUG输出调试 一、预备知识: WB系列是双核单片机,用户写M4,无线协议栈使用M0新买到手的单片机,需要自己刷入使用的…

TF-IDF,textRank,LSI_LDA 关键词提取

目录 任务 代码 keywordExtract.py TF_IDF.py LSI_LDA.py 结果 任务 用这三种方法提取关键词,代码目录如下, keywordExtract.py 为运行主程序 corpus.txt 为现有数据文档 其他文件,停用词,方法文件 corpus.txt 可以自己…

DP读书:《半导体物理学(第八版)》(一)绪论 3min速通

DP读书:《半导体物理学(第八版)》刘恩科 3min速通半导体物理之绪论 DP读书:《半导体物理学(第八版)》刘恩科绪论第一章 半导体中的电子状态1.1 半导体的晶格结构和结合性质1.1.1 金刚石型结构和共价键1.1.2…

【电机仿真】HFI算法脉振高频电压信号注入观测器-PMSM无感FOC控制

【电机仿真】HFI算法脉振高频电压信号注入观测器-PMSM无感FOC控制 文章目录 前言一、脉振高频电压注入法简介(注入在旋转坐标系的d轴)1.旋转高频电压(电流)注入法2.脉振高频电压注入法 二、高频注入理论1.永磁同步电机的高频模型2…

Pyglet控件的批处理参数batch和分组参数group简析

先来复习一下之前写的两个例程: 1. 绘制网格线 import pygletwindow pyglet.window.Window(800, 600) color (255, 255, 255, 255) # 白色 lines []for y in range(0, window.height, 40):lines.append(pyglet.shapes.Line(0, y, window.width, y, colorcolo…

LeetCode704. 二分查找(C++)

LeetCode704. 二分查找 题目链接代码 题目链接 https://leetcode.cn/problems/binary-search/description/ 代码 class Solution { public:int search(vector<int>& nums, int target) {int left 0;int right nums.size() - 1;while(left < right){int midd…

外包工作两个月,技术退步让我决心改变

大家好&#xff0c;我是一名大专生&#xff0c;2019年通过校招进入了湖南的一家软件公司。在这里&#xff0c;我从事了接近4年的功能测试工作。然而&#xff0c;今年8月份&#xff0c;我深刻地意识到&#xff0c;我不能继续这样下去了。 长时间在一个舒适的环境里&#xff0c;…

数据库系统概论(超详解!!!) 第一节 绪论

1.四个基本概念 1.数据&#xff08;Data&#xff09; 数据&#xff08;Data&#xff09;是数据库中存储的基本对象 数据的定义&#xff1a;描述事物的符号记录 数据的种类&#xff1a;数字、文字、图形、图像、音频、视频、学生的档案记录等 数据的含义称为数据的语义&…

记生产OOM的故障分析

一、引言 生产上告警&#xff0c;交易堵塞&#xff0c;服务无响应&#xff0c;使用jstack、jmap、jhat命令进行故障分析。 Java虚拟机&#xff08;Java Virtual Machine&#xff0c;简称JVM&#xff09;作为Java语言的核心组件&#xff0c;为Java程序提供了运行环境和内存管理…

docker存储驱动

目录 一、写时复制和用时分配 二、联合文件系统 2.1、aufs ​编辑 2.2、分层的问题 2.3、overlay 2.4 文件系统区别 三、容器跑httpd案例 3.1、案例1&#xff1a;端口映射 3.2、案例2&#xff1a;制作httpd应用镜像 3.3、案例3&#xff1a;docker数据卷挂载 3.4、案…

【hot100】跟着小王一起刷leetcode -- 49. 字母异位词分组

【【hot100】跟着小王一起刷leetcode -- 49. 字母异位词分组 49. 字母异位词分组题目解读解题思路代码实现 总结 49. 字母异位词分组 题目解读 49. 字母异位词分组 ok&#xff0c;兄弟们&#xff0c;咱们来看看这道题&#xff0c;很明显哈&#xff0c;这里的关键词是字母异位…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-27-处理单选和多选按钮-番外篇

1.简介 前边几篇文章是宏哥自己在本地弄了一个单选和多选的demo&#xff0c;然后又找了网上相关联的例子给小伙伴或童鞋们演示了一下如何使用playwright来处理单选按钮和多选按钮进行自动化测试&#xff0c;想必大家都已经掌握的八九不离十了吧。这一篇其实也很简单&#xff1a…

浅谈 TCP 三次握手

文章目录 三次握手 三次握手 首先我们需要明确&#xff0c;三次握手的目的是什么&#xff1f; 是为了通信双方之间建立连接&#xff0c;然后传输数据。 那么建立连接的条件是什么呢&#xff1f; 需要确保通信的双方都确认彼此的接收和发送能力正常&#xff0c;满足这个条件&a…

今天面了个字节拿 38K 出来的测试,让我见识到了基础的天花板

最近内卷严重&#xff0c;各种跳槽裁员&#xff0c;相信很多小伙伴也在准备金九银十的面试计划。 作为一个入职5年的老人家&#xff0c;目前工资比较乐观&#xff0c;但是我还是会选择跳槽&#xff0c;因为感觉在一个舒适圈待久了&#xff0c;人过得太过安逸&#xff0c;晋升涨…

Jeecg项目部署

说明&#xff1a;Jeecg是一款低代码开发平台&#xff0c;简单说是一款现成的项目&#xff0c;该项目集成了许多功能&#xff0c;我们可以在这个项目之上开发自己的业务代码。 本文介绍Jeecg项目的部署&#xff0c;包括后端jeecg-boot项目、前端vue3项目。前端项目在本地Window…

Java的编程之旅19——使用idea对面相对象编程项目的创建

在介绍面向对象编程之前先说一下我们在idea中如何创建项目文件 使用快捷键CtrlshiftaltS新建一个模块&#xff0c;点击“”&#xff0c;再点New Module 点击Next 我这里给Module起名叫OOP,就是面向对象编程的英文缩写&#xff0c;再点击下面的Finish 点Apply或OK均可 右键src…

2024Python自动化测试面试必备知识点!

在准备 Python 自动化测试面试时&#xff0c;以下是一些必备的知识点&#xff0c;可以帮助您在面试中展现实力&#xff1a; 软件测试基础&#xff1a; 熟悉软件测试的基本概念&#xff0c;包括测试类型&#xff08;功能测试、性能测试、安全测试等&#xff09;、测试方法&#…

数据安全治理实践路线(中)

数据安全建设阶段主要对数据安全规划进行落地实施&#xff0c;建成与组织相适应的数据安全治理能力&#xff0c;包括组织架构的建设、制度体系的完善、技术工具的建立和人员能力的培养等。通过数据安全规划&#xff0c;组织对如何从零开始建设数据安全治理体系有了一定认知&…

微服务篇之任务调度

一、xxl-job的作用 1. 解决集群任务的重复执行问题。 2. cron表达式定义灵活。 3. 定时任务失败了&#xff0c;重试和统计。 4. 任务量大&#xff0c;分片执行。 二、xxl-job路由策略 1. FIRST&#xff08;第一个&#xff09;&#xff1a;固定选择第一个机器。 2. LAST&#x…