英文:
Package/directory structuring for Interfaces and their implementations
问题
让我们考虑一个典型的Web应用程序。在MVC应用程序中,您可能最终希望引入一个“服务”层,用于抽象复杂的业务逻辑,比如用户注册。因此,在控制器中,您会传递一个services.User
结构的实例,并简单地调用其上的Register()
方法。
现在,如果services.User
只是一个结构体,我们可以有一个相对简单的源代码结构,如下所示:
- [其他目录]/
- services/
- user.go
- [其他服务结构体]
- main.go
services/user.go
的内容如下:
package services
type User struct { ... }
func NewUserService(){ ... }
func (u User) Register() { ... }
到目前为止,这一切都相当容易阅读。现在,假设我们进一步进行改进。为了使我们的Web应用程序易于测试,我们将所有的Service结构体转换为Service接口。这样,我们就可以轻松地为单元测试创建它们的模拟对象。为此,我们将创建一个“AppUser”结构体(用于实际应用程序)和一个“MapUser”结构体(用于模拟目的)。将接口和实现放在同一个services
目录中是有道理的,毕竟它们都是service
代码。
现在,我们的services
文件夹看起来像这样:
- services/
- app_user.go // AppUser结构体
- [其他服务]
- map_user.go // MapUser结构体
- [其他服务]
- user.go // User接口
- [其他服务结构体]
正如您所看到的,这使得services
包和目录变得更加难以处理 - 您可以想象一下,如果有十几个不同的接口,每个接口至少有一个实现,那么它看起来会多么混乱。如果我更改user.go
中的User
接口,我将不得不在整个目录列表中查找所有实现并进行更改,这绝对不是理想的情况。
此外,当您键入services.New(...)
时,可能会看到大约50个自动完成建议;services
包已经变成了一个庞大的怪物。
我想到的最简单的解决方法之一是违背常规并接受重复:
- services/
- userService/
- app.go // AppUser结构体
- map.go // MapUser结构体
- interface.go // User接口
- [其他服务]
这样可以将所有与UserService相关的代码放在一个逻辑上自包含的包中。但是,不断引用userService.UserService
确实很丑陋。
我查看了各种Web应用程序模板,除了那些非常简单的模板之外,没有一个能够提供这种结构的优雅解决方案。大多数(如果不是全部)模板都完全省略了接口来解决这个问题,这是不可接受的。
英文:
Let's consider your typical web application. In an MVC application, you may eventually want to introduce a "Service" layer that abstracts complex business logic such as user registration. So in your controller, you'd pass an instance of a services.User
struct and simply call the Register()
method on it.
Now, if services.User
was simply a struct, we could have a relatively simple source code structure, like so:
- [other directories here]/
- services/
- user.go
- [other service structs here]
- main.go
And the services/user.go
would look like so :
package services
type User struct { ... }
func NewUserService(){ ... }
func (u User) Register() { ... }
Which is all reasonably easy to read, so far. Let's say we take it one step further. In the spirit of making our web app easily testable, we'll turn all our Service structs into Service interfaces. That way, we can easily mock them for unit tests. For that purpose, we'll create a "AppUser" struct (for use in the actual application) and a "MapUser" struct (for mocking purposes). Placing the interfaces and the implementations in the same services
directory makes sense - they're all still service
code, after all.
Our services
folder now looks like this:
- services/
- app_user.go // the AppUser struct
- [other services here]
- map_user.go // the MapUser struct
- [other services here]
- user.go // the User interface
- [other service structs here]
As you can tell, this makes the services
package and directory a lot more difficult to handle - you can easily imagine how chaotic it would look with a dozen different interfaces, each of which at least have at least 1 implementation. If I change the User
interface in user.go
, I'd have to dart all across the directory listing to find all it's implementations to change, which is not at all ideal.
Additionally, it becomes pretty crazy when you type services.New(...)
and you're greeted with perhaps 50 or so autocomplete suggestions ; the services
package has become nothing but a shambling monster.
One of the simplest ideas I had to solve this is to go against convention and embrace repetition:
- services/
- userService/
- app.go // the AppUser struct
- map.go // the MapUser struct
- interface.go // the User interface
- [other services here]
This keeps all the UserService related code in a logical, self contained package. But having to constantly refer to userService.UserService
is pretty darn ugly.
I've looked at all kinds of web application templates, and none of them (beyond the ones that are incredibly barebones) have an elegant solution to this structural. Most (if not all) of them simply omit interfaces completely to solve it, which is unacceptable.
答案1
得分: 6
你的接口实现应该(通常)位于单独的包中。这不是一个严格的规则,你可能经常在接口定义旁边有一个默认实例。
但是想象一个更抽象的例子:一个键/值存储接口。它可以由文件系统、SQL数据库、Amazon S3或内存数据结构支持。
通常情况下,你会在一个地方定义接口,比如 myproject/kvstore/kvstore.go
。
然后你会在其他地方定义你的实现,甚至可能在完全不同的代码库中。
- myproject
- kvstore
kvstore.go
memory.go -- 一个默认实现,非持久化
- filesystem
filesystem.go -- 一个文件持久化实现
- yourproject
- sqlite -- 一个由sqlite支持的实现
在你的具体例子中,至少我会将实现存储在接口定义的下一级目录中:
- services/
- userService/
- interface.go // User接口
- app
app.go // AppUser结构体
- map
map.go // MapUser结构体
- [其他服务在这里]
这样就不会混淆 map.New()
和 app.New()
,你的内部数据结构也不会相互干扰。
英文:
Your interface implementations should (generally) live in separate packages. This isn't a hard and fast rule, and you may often have a default instance live next to the interface definition.
But think of a more abstract example: a Key/Value store interface. It may be backed by the filesystem, a SQL database, Amazon S3, or an in-memory data structure.
You'd typically define your interface in one place, say myproject/kvstore/kvstore.go
.
Then you'll define your implementations elsewhere. Possibly even in entirely different repositories.
- myproject
- kvstore
kvstore.go
memory.go -- A default implementation, non-persistent
- filesystem
filesystem.go -- A file-persistent implementation
- yourproject
- sqlite -- An implementation backed by sqlite
In your specific example, at minimum I would store my implementations one level beneath the interface definition:
- services/
- userService/
- interface.go // the User interface
- app
app.go // the AppUser struct
- map
map.go // the MapUser struct
- [other services here]
Then there's no confusion between map.New()
and app.New()
, and your internal data structures won't step on each other, etc.
答案2
得分: 2
另一种可能的方法是重新安排你的包结构。不再使用services
目录下的对象集合来表示更强大的功能:
- users/
- app.go
- map.go
- interface.go
这样可以支持一个users.New()
方法,该方法将被注册为委托给默认实现。这样可以将存根/内存接口与生产实现分开存放。
如果你需要一个服务注册表或其他类似的东西,可以将其作为一个独立的抽象,其中注册了users.New()
方法,或者其他类似的方法。
英文:
Another possible approach could be to rearrange your packages. Instead of services
having more empowered collections of objects, reflected in directory structure:
- users/
- app.go
- map.go
- interface.go
Which could support a users.New()
method which would be registered to delegate to the default. This could allow you to keep stub/memory based interfaces in separate files from the production implementations.
If you needed a service registry or something that could be a separate abstraction which has users.New()
registered, or something?
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论