“接受接口、返回结构” 的一般原则,我在前一篇文章中写到,也多次在代码评审时向同事介绍,但经常遇到“为什么”的疑问。特别是因为这不是一条硬性规定。该想法的关键在于保持灵活性的同时避免预先抽象,并理解何时改变它。
预先抽象使系统变得复杂
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,当然,间接过多的问题除外 - David J. Wheeler
软件工程师喜欢抽象。就我个人而言,从未见过编写代码比创建抽象更投入的同事。Go 中,接口抽象脱离了结构,该间接层甚至没有最低层次的嵌入复杂性。遵循软件设计 您不会用到它 的哲学,在需要之前制造这种复杂性毫无意义。函数调用返回接口的一个常见原因是让用户专注于函数开放的 API。因为有隐式接口,Go 不需要这样做。结构的 public function iu 就是其 API。
总是当 真正 需要时 [抽象],不要当 预见 需要时 [抽象]。
某些语言要求你预见未来需要的每个接口。隐式接口的一大优点是,它们允许事后进行优雅的抽象,而无需预先进行抽象。
因人而异的“需要”
当真正需要时
如何定义何时需要抽象?对于返回类型,这很容易。你是编写该函数的人,因此您确切知道何时需要将返回值抽象。
对于函数输入,需求不在你的控制范围之内。你可能认为 database struct 就足够了,但用户可能需要用其他东西装饰它。就算不是不可能,预测每个人使用你的函数的状态也是很困难的。能够精确控制输出,但无法预测用户输入。相比对输出的抽象化,这种不平衡造成了对输入的抽象化更强烈的偏重。
去除不必要的代码细节
简化的另一方面是去除不必要的细节。函数就像烹饪食谱:给定输入,就会得到一个蛋糕!没有食谱会列出不需要的配料。类似地,函数也不应该列出不需要的输入。你如何看以下函数?
func addNumbers(a int, b int, s string) int { return a + b }
对于大多数程序员来说,很明显,参数 s 不恰当。当参数是结构时,却不太明显。
type Database struct{ } func (d *Database) AddUser(s string) {...} func (d *Database) RemoveUser(s string) {...} func NewUser(d *Database, firstName string, lastName string) { d.AddUser(firstName + lastName) }
就像配料太多的食谱一样,NewUser 接收一个可以做太多事情的 Database 对象。它只需要 AddUser,但接收的参数还有 RemoveUser。使用接口创建的函数,可以只依赖于必需。
type DatabaseWriter interface { AddUser(string) } func NewUser(d DatabaseWriter, firstName string, lastName string) { d.AddUser(firstName + lastName) }
Dave Cheney 在描述 接口隔离原则 时 写到了这一点。他还描述了限制输入的其他优点,值得一读。让人理解这个想法的总目标是:
结果同时是一个函数,它的要求是最具体的——它只需要一个可写的东西——并且它的函数是最通用的
我只想补充一点,上面的函数 addNumber 显然不应该有参数字符串 s,函数 NewUser 理想情况下不需要可以删除用户的 database。
总结原因并审查例外
主要原因如下:
- 去除不需要的抽象
- 用户对函数输入需求是模糊的
- 简化函数输入
以上原因还允许我们定义规则的例外情况。例如,如果函数需要返回多种类型,那么显然返回需要定义为接口。类似地,如果函数是私有的,那么函数输入便并不模糊,因为你可以控制它,所以倾向于非预先抽象。对于第三条规则,go 没有办法抽象出 struct 成员的值。因此,如果你的函数需要访问结构体成员(而不仅仅是结构体上的函数),那么您将被迫直接接受结构体。
原文:What “accept interfaces, return structs” means in Go
本文作者 : cyningsun
本文地址 : https://www.cyningsun.com/08-08-2021/go-accept-interfaces-return-structs.html
版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!