3

我试图在使用 Bob Martin 叔叔的“清洁架构”设计的 Go 程序中为我的 ID 找到合适的类型。

type UserID ...

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

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

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

我的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)。

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

4

3 回答 3

1

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

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 中隐藏字符串转换可能更优雅?

于 2016-07-06T13:08:50.933 回答
0

真的User需要加上身份吗?

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

这允许我选择一种在我测试我的用例时方便的身份(UUID)(并为存储库接口编写一个模拟实现)和另一种在我与数据库通信时方便的身份(整数) .

无论哪种方式,我的数据类都保持不变,我发现这非常方便。

(在某些时候,我确实在我的数据类中将标识保留为泛型,但我认为有些东西 - 也许是 equals 和 hashCode 的实现,不记得了 - 迫使我决定标识的类型)

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

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

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

并且对依赖于 UserRepository 的用例的测试将使用 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) {
        ...
    }
}

澄清:

我想要完全控制主键,所以我不允许它们在数据库适配器之外。

系统仍然需要能够识别实体,所以我必须介绍一些别的东西。通常这只是一个简单的 UUID。

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

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

于 2018-04-07T17:37:57.280 回答
0

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

  • 将 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/

于 2020-06-26T23:32:23.000 回答