【问题标题】:Package/directory structuring for Interfaces and their implementations接口及其实现的包/目录结构
【发布时间】:2017-09-11 15:48:45
【问题描述】:

让我们考虑一下您的典型 Web 应用程序。在 MVC 应用程序中,您最终可能希望引入一个“服务”层,该层抽象了复杂的业务逻辑,例如用户注册。因此,在您的控制器中,您将传递一个 services.User 结构的实例,并在其上简单地调用 Register() 方法。

现在,如果services.User 只是一个结构,我们可以有一个相对简单的源代码结构,如下所示:

- [other directories here]/
- services/
    - user.go
    - [other service structs here]
- 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 // the AppUser struct
    - [other services here]
    - map_user.go // the MapUser struct
    - [other services here]
    - user.go  // the User interface
    - [other service structs here]

如您所知,这使得services 包和目录更难处理 - 您可以轻松想象如果有十几个不同的接口,它看起来会多么混乱,每个接口至少有 1 个实现。如果我更改user.go 中的User 接口,我必须在整个目录列表中查找所有要更改的实现,这一点都不理想。

此外,当您输入services.New(...) 时,您会收到大约 50 条左右的自动完成建议,这会变得非常疯狂; services 包已经变成了一个蹒跚的怪物。

我必须解决的最简单的想法之一是违反惯例并接受重复:

- services/
    - userService/
        - app.go // the AppUser struct
        - map.go // the MapUser struct
        - interface.go  // the User interface
    - [other services here]

这将所有与 UserService 相关的代码保存在一个逻辑的、自包含的包中。但是不得不经常引用userService.UserService 实在是太丑陋了。

我查看了各种 Web 应用程序模板,但没有一个(除了那些令人难以置信的准系统)对这种结构有一个优雅的解决方案。他们中的大多数(如果不是全部)只是完全省略了接口来解决它,这是不可接受的。

【问题讨论】:

    标签: go interface


    【解决方案1】:

    您的接口实现应该(通常)存在于单独的包中。这不是一个硬性规定,您可能经常在接口定义旁边有一个默认实例。

    但是想一个更抽象的例子:Key/Value 存储接口。它可能由文件系统、SQL 数据库、Amazon S3 或内存数据结构支持。

    你通常会在一个地方定义你的界面,比如myproject/kvstore/kvstore.go

    然后您将在别处定义您的实现。甚至可能在完全不同的存储库中。

    - 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
    

    在您的具体示例中,我至少会将我的实现存储在接口定义的下一级:

    - services/
        - userService/
            - interface.go  // the User interface
            - app
                app.go // the AppUser struct
            - map
                map.go // the MapUser struct
        - [other services here]
    

    那么map.New()app.New()之间就不会混淆了,你的内部数据结构也不会互相踩踏等等。

    【讨论】:

    • 所以如果我们想访问MapUser 实现,它会是map.MapUser 吗?我猜不是,因为有重复,而且“MapUser”不再告诉我们它是一项服务。还是直接采用接口名,变成map.UserService?例如,如果所有其他服务也都有“map”实现,那么它们不是都存在于“map”包中,从而使“map.New()”变得模棱两可吗?
    • @aetheus:是的,你的map.MapUser(或者map.User,如果你喜欢一个不那么冗余的名字)是你的服务接口的实现。从技术上讲,你给它起什么名字并不重要。
    • @aetheus:如果您有不同的地图服务实现,我建议将它们放在单独的包中。适当地命名它们以避免混淆。 mapuser.New()mapfoo.New()
    【解决方案2】:

    另一种可能的方法是重新排列您的包裹。而不是 services 拥有更多授权的对象集合,反映在目录结构中:

    - users/
       - app.go
       - map.go
       - interface.go
    

    它可以支持users.New() 方法,该方法将被注册为委托给默认值。这可以让您将基于存根/内存的接口保存在与生产实现不同的文件中。

    如果您需要一个服务注册中心或其他可以是注册了users.New() 的单独抽象的东西,或者其他什么?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多