"Clean Architecture" Go 程序的通用 ID 类型

Generic ID type for "Clean Architecture" Go program

我正在尝试在使用 Uncle Bob Martin "Clean Architecture" 设计的 Go 程序中为我的 ID 找到合适的类型 "Clean Architecture"。

type UserID ...

type User struct {
  ID UserID
  Username string
  ...
}

type UserRepository interface {
  FindByID(id UserID) (*User, error)
  ...
}

我正在关注 Uncle Bob Martin 的“Clean Architecture”,其中代码被组织为一组层(从外到内:基础结构接口用例)。其中一项原则是依赖规则:源代码依赖只能指向内部

我的 User 类型是域层的一部分,因此 ID 类型不能依赖于为 UserRepository 选择的数据库;如果我使用 MongoDB,ID 可能是 ObjectId (string),而在 PostgreSQL 中,我可能使用整数。 User 域层中的类型无法知道实现类型是什么。

通过依赖注入一个真正的类型(例如MongoUserRepository)将实现UserRepository接口并提供FindByID方法。由于此 MongoUserRepository 将在接口或基础设施层中定义,因此它可以依赖于(更向内的)域层中 UserRepository 的定义。

我考虑过使用

type UserID interface{}

但是如果外层之一的代码试图分配不正确的实现类型,编译器将不会很有帮助。

我想要指定数据库的接口层或基础设施层,确定并要求 UserID 的特定类型,但我不能让域层代码导入该信息,因为那样会违反依赖规则。

我也考虑过(目前正在使用)

type UserID interface {
    String() string
}

但这假设知道数据库将使用字符串作为其 ID(我使用 MongoDB 及其 ObjectId —— string 的类型同义词)。

如何以惯用的方式处理这个问题,同时允许编译器提供最大的类型安全性并且不违反依赖性规则?

也许你可以使用这样的东西:

type UserID interface {
  GetValue() string
  SetValue(string)
}

然后您假设您始终将字符串作为 ID 传递和获取(在 PgSQL 和其他 RDBMS 的情况下,它可以是整数 ID 的字符串化版本),并且您为每个数据库类型实现 UserID:

type PgUserID struct {
    value int
}

func (id *PgUserID) GetValue() string {
    return strconv.Itoa(id.value)
}

func (id *PgUserID) SetValue(val string){
    id.value = strconv.Atoi(val)
}

type MongoUserID struct {
    value string
}

func (id *MongoUserID) GetValue() string {
    return value
}

func (id *MongoUserID) SetValue(val string){
    id.value = val
}

我想知道这是否实现了您想要实现的目标,但也许将字符串转换隐藏在 UserID 中更优雅?

User真的需要加上身份吗?[=​​14=]

我不知道这是聪明还是愚蠢,但我已经停止在数据中添加身份属性 classes 并且仅将其用作访问信息的方式,就像您使用地图或字典。

这允许我选择一种身份类型 (UUID) 在我测试 use-cases 时很方便(并为存储库接口编写一个模拟实现)和另一种类型的身份(整数)方便我和数据库通信。

无论如何,我的数据 class 保持不变,我发现这非常方便。

(在某些时候,我确实在我的数据中保留了通用身份 class 但我认为有些事情 - 也许是 equals 和 hashCode 的实现,不记得了 - 迫使我决定身份类型)

我主要使用 Java 编写代码并且对 Go 一无所知,但是在 Java 我会使用:

public class User {
    private final String username;
    ...
}

public interface UserRepository<K> {
    public User findById(K identity)
}

并在依赖于 UserRepository 的 use-cases 上进行测试将使用 UserRepository 的模拟实现,并将 UUID 作为身份类型:

public class UserRepositoryMock implements UserRepository<UUID> {
    public User findById(UUID identity) {
        ...
    }
}

但是我针对实际数据库的 UserRepository 实现将使用 Integer 作为标识类型:

public class UserSQLRepository implements UserRepository<Integer> {
    public User findById(Integer identity) {
        ...
    }
}

澄清:

我想要完全控制主键,所以我不允许它们超出 database-adapter。

系统还需要能够识别实体,所以我不得不介绍一些其他的东西。通常这只是一个简单的 UUID。

这不是免费的。在database-adapter中我要引入主键和身份之间的转换。所以两者都存储为表中的列。这也意味着在我可以在查询中使用主键之前,我必须从数据库中进行一次额外的读取。

(我只使用过较小的系统,所以我不知道这在较大的系统中是否可以接受trade-off)

我遇到了同样的问题,根据一些阅读,这就是我打算使用的。

  • 将 ID 生成保留在域之外。
  • 由于存储库处理所有条目,因此存储库生成新 ID 很有意义。
class UserId:
    # Hides what kind of unique-id is being used and lets you change it without affecting the domain logic.
    pass


class User:
    uid: UserId
    name: str


class UserRepository:
    def next_id() -> UserId:
        # Can use UUID generator
        # Or database based sequence number
        pass


class UserUsecase:
    def __init__(self, repository):
        self.repository = repository

    def create(self, name):
        user_id = self.repository.next_id()
        user = User(user_id, name)
        self.repository.save(user)
        return user

参考: https://matthiasnoback.nl/2018/05/when-and-where-to-determine-the-id-of-an-entity/