英文:
Interface organization in Go project
问题
我不太清楚这个Go wiki的说法:“Go接口通常属于使用接口类型值的包,而不是实现这些值的包”,是否正确,接口是在使用它们的包中声明的吗?
例如,如果我有一个test.go文件:
package test
type Tester struct {...}
func NewTester(){
return Tester{...}
}
func(t * Tester) GetCache(key string) (value string, error){...}
func(t * Tester) GetDB(key string) (value string, error){...}
现在,假设我有20个包在其中使用GetCache方法。我需要在这20个包中分别定义接口吗,像这样:
type TestCache interface {
GetCache(key string) (value string, error)
}
这样真的可维护吗?如果GetCache更改了接口契约,我需要更新所有20个包中的声明。在定义方法的包中声明每个接口会不会更好呢?
英文:
I don't understand so clear this Go wiki statement: “Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values”, is it correct that interfaces are declared where the are used?
if for example i have this test.go file
package test
type Tester struct {...}
func NewTester(){
return Tester{...}
}
func(t * Tester) GetCache(key string) (value string, error){...}
func(t * Tester) GetDB(key string) (value string, error){...}
And now, let's say I have 20 packages where I use the GetCache method. Do I need to define the interface in each of the 20 packages, like this:
type TestCache interface {
GetCache(key string) (value string, error)
}
Is it really maintainable? What if GetCache changes the interface contract? I would need to update all 20 declarations in the packages. Wouldn't it be better to declare each interface within the package where the methods are defined?
答案1
得分: 1
是的,你的陈述是正确的 - 你可能希望将接口存储在实际依赖它们的对象附近,并且由于 Golang 接口的"隐式"特性,这是可能实现的。来自典型面向对象编程语言背景的工程师(比如我自己从 C# 转过来的)通常很难理解这种语言特性的目的。我可以详细说明一下你描述的结构为什么是有意义的。
想象一下,在你的库中有这样一个实体:
type Lib struct {
Foo(){}
Foo2(){}
Foo3(){}
Foo4(){}
}
type LibInterface interface {
Foo()
Foo2()
Foo3()
Foo4()
}
如果你在同一个包中定义一个接口,所有的用户都将注入 Lib
暴露的这 4 个方法,即使其中一些用户只需要 1 或 2 个方法。所以你明确地告诉别人:"嘿,我需要那个库的这 4 个方法",但事实并非如此,因为你只使用了其中的 1 个。现在为它编写单元测试:
type LibUser struct{
lib LibInterface
}
func New(lib LibInterface) *LibUser {
lib = lib
}
你需要为你的依赖定义一个模拟对象:
type LibMock struct{}
func (LibMock) Foo(){}
func (LibMock) Foo1(){}
func (LibMock) Foo2(){}
func (LibMock) Foo3(){}
func (LibMock) Foo4(){}
所以你为这个接口暴露的所有 4 个方法定义了一个模拟对象,即使你只使用了其中的 1 个。接下来会发生什么呢?当 Lib
引入一个新的方法时,你需要去更新所有使用该库的消费者的模拟对象,即使你仍然不使用它们!这不是很烦人吗?
当然,你可以使用像 Mockery 这样的工具,将模拟对象与接口和实现放在一起,并在所有测试中引入整个代码块。但是...这样干净吗?Go 提供了一种更干净的实现方式。当 LibUser
为其依赖定义自己的接口时,非常明确地表明了它使用的内容。此外,如果你来自 Java 或 C# 的背景,你会记得没有抽象层时是多么烦人,因此你无法对它们进行模拟测试,只能没有测试或者创建自己的包装器!在 Go 中,这个问题是不存在的。对我来说,消费者声明接口听起来像是一种干净的方法。但是再次强调 - 这不是一个规则,你可以自行决定如何组织你的项目。
这些是我在使用 Go 构建了大量服务和库,并且来自"面向对象编程"背景的个人反思。
英文:
Yes, your statement is correct - you may want to store your interfaces close to the objects that actually depend on them and it is possible to implement it due to the implicit
nature of the Golang interfaces. Engineers coming from typical OOP language backgrounds (like myself coming from C#) usually have a hard time grasping what is the purpose of this language feature.
I can elaborate on why the structure you describe makes sense.
Imagine you have an entity like this in your library:
type Lib struct {
Foo(){}
Foo2(){}
Foo3(){}
Foo4(){}
}
type LibInterface interface {
Foo()
Foo2()
Foo3()
Foo4()
}
If you define an interface in the same package, all users will be injecting all 4 methods Lib
exposes, even if some of the users need only 1 or 2. So you explicitly telling: "Hey I need these 4 methods from that library", which is not true because you only use 1. Now write unit tests for it:
type LibUser struct{
lib LibInterface
}
func New(lib LibInterface) *LibUser {
lib = lib
}
You would need to define a mock for your dependency:
type LibMock struct{}
func (LibMock) Foo(){}
func (LibMock) Foo1(){}
func (LibMock) Foo2(){}
func (LibMock) Foo3(){}
func (LibMock) Foo4(){}
So you define a mock for all 4 methods interface exposes even though you use only 1. What comes next? Lib introduces a new method - you go to ALL your lib consumers and update mocks even though you still do not use them! Isn't it annoying?
Of course, you can use tools like Mockery and keep mocks together with the interface and implementation and pull that entire blob in all your tests. But... is it clean? Go gives you the ability to implement it in a cleaner way. When LibUser
defines its own interfaces for its dependencies it is very explicit what it uses. Also, if you are coming from Java or C# background you remember how annoying it is when a library does not have abstractions so you cannot mock them and you simply need to live without tests or create your own wrappers! So this problem does not exist in Go. Declaring interfaces by consumers to me sounds like a clean approach. But again - it is not a rule, you can decide on your own how you want to structure your projects.
Those are my personal reflections after building plenty of services and libraries written in Go and coming from OOP
background where interfaces are explicit
答案2
得分: 0
你可以在其他包中使用类似以下的代码:
var _ TestCache = &Tester{}
如果你改变了Tester的实现,程序将无法再编译通过。
请参考:https://go.dev/play/p/L4bQYBcr4jv
该包不依赖于具体的实现,而是依赖于同一包中定义的接口。这要归功于接口的鸭子类型。
是的,你可以为接口定义另一个包,并在那里定义接口,然后依赖于它。但在这种情况下,耦合度会更高。
此外,接口命名约定规定Tester应该是一个接口名称,而不是结构体。请参考:https://go.dev/doc/effective_go#interface-names
英文:
You can use something like
var _ TestCache = &Tester{}
in other packages. Program will not compile any more, if you change Tester implementation
See https://go.dev/play/p/L4bQYBcr4jv
Package not depends on concrete implementation, but on interace defined by same package. Thanks to interface duck typing.
Yes, you can define another one package for interface, define interface there, and depends on it. But coupling will be higher in this case.
*Also, interface naming conventions says Tester should be an interface name, not struct https://go.dev/doc/effective_go#interface-names
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论