【问题标题】:What is the best practice for designing entities in Clean Architecture?在 Clean Architecture 中设计实体的最佳实践是什么?
【发布时间】:2021-06-08 17:38:17
【问题描述】:

我正在尝试使用 Kotlin 实现干净的架构。流程将是:

usecase --> get rowresult from DB --> map rowresult to entity --> entity used by the usecase to check business rules

代码示例:

UserTable
------------------
id (varchar)
email (varchar)
password (varchar)
gender (varchar)
phone (varchar)
anotherAttribute1
anotherAttribute2
.
anotherAttributeN
class UserEntity {
    val id: String,
    val email: String,
    val password: String,
    //Business rules
    fun isUserAllowedToLogin(): Boolean {
        //validate password
    }
}

interface UserDataStore {
    fun getUser(email: String): User
}

class UserDataStoreImplementation {
    fun getUser(email: String): User {
        //query to DB
        val resultRow = db.query("SELECT id, email, password from UserTable where email=${email}")
        //map to UserEntity
        val user: UserEntity = Mapper.toUserEntity(userResultRow)
        return user
    }
}

class LoginUseCase {
    fun execute(emailInput: String, passwordInput: String): Boolean {
        val user = UserDataStore().getUser(emailInput)
        if (!user.isUserAllowedToLogin) {
            //do something
        }
        return result
    }
}

请注意 loginUseCase 使用的唯一属性是用户电子邮件和密码。

问题 1. 假设如果我有另一个 UseCase (GetUserFullDetailAndStaffDetail Usecase) 将使用更复杂的 User 属性,我应该对 GetUserFullDetailAndStaffDetail 用例使用相同的 UserEntity 吗?所以 UserEntity 将是:

class UserEntity {
    val id: String,
    val email: String,
    val password: String,
    val gender: String,
    val phone: String,
    //more attributes
    .
    .
    //more complex object
    val Staff: Staff
    fun isUserAllowedToLogin(): Boolean {
        //validate password
    }
    fun checkStaffStatus(): Boolean {
        //do something
    }
}

class UserDataStoreImplementation {
    fun getUser(email: String): User {
        //query from DB which will have a lot of attributes
        val resultRow = db.query("SELECT * from UserTable where email=${email}")
        //map to UserEntity
        val user: UserEntity = Mapper.toUserEntity(userResultRow)
    }
}

如果我使用不同的实体,它将违反 DRY 原则(在 UserDataStoreImplementation 中重复 UserEntity 和重复 getUser 方法),但如果我对 GetUserFullDetailAndStaffDetail 用例使用相同的 UserEntity,则在 UserDataStoreImplementation 中为 LoginUseCase 的 getUser 必须获得无用的完整属性。

问题2. UserDataStoreImplementation 中的getUser 是否应该有不同的方法(一种会在UserTable 中为LoginUseCase 返回部分属性,另一种会在UserTable 中为GetUserFullDetailAndStaffDetail UseCase 返回完整属性)?

【问题讨论】:

    标签: architecture software-design clean-architecture


    【解决方案1】:

    问题 1. 假设我有另一个 UseCase (GetUserFullDetailAndStaffDetail Usecase) 将使用更复杂的 User 属性,我应该为 GetUserFullDetailAndStaffDetail 用例使用相同的 UserEntity 吗?

    实体是域对象,LoginUserUserDetail 不同。我们经常认为有一个User。但是用户有不同的看法。您可以将它们视为一种角色。

    public class LoginUser {
      private String name;
      private String email;
    
      // ...
    }
    

    DetailUser

    public class DetailUser {
      private String name;
      private String email;
      private String phone; 
      private String gender;
      // ...
    }
    

    如您所见,我省略了id。通常它是数据库详细信息而不是域属性。但有时它是,例如就像一个客户号码。

    如果我使用不同的实体,它将违反 DRY 原则(在 UserDataStoreImplementation 中重复 UserEntity 和重复 getUser 方法),但如果我对 GetUserFullDetailAndStaffDetail 用例使用相同的 UserEntity,则在 UserDataStoreImplementation 中为 LoginUseCase 的 getUser 必须获得无用的完整属性。

    它不违反干原则,因为你不会重复自己。我同意我们必须摆脱重复的代码,但是LoginUser 会因为不同的原因而改变,然后是DetailUser。因此它们不会重复。这就是单一职责的意义所在。

    它们看起来很相似,但相似度只是重复的提示。您必须问自己更多问题才能确定它们是否真的重复。让我们考虑一下登录用例的更改。也许应该只显示名称。那么这两个实体将只有一个共同的属性 - 名称。他们必须有多少共同的属性才能被复制?

    如果您在实体中实现业务逻辑,您将意识到有些方法只会在两个用例之一中调用,并且这些方法只使用属性的子集。然后你会发现这两个实体是不同的。

    问题2. UserDataStoreImplementation 中的getUser 是否应该有不同的方法(一种会在UserTable 中为LoginUseCase 返回部分属性,另一种会在UserTable 中为GetUserFullDetailAndStaffDetail UseCase 返回完整属性)?

    我会说每个用例都应该定义它自己的存储库接口。这个接口应该只定义这个用例需要的方法。这是接口隔离原则的一种应用,它尊重单一职责原则。

    就像您指出的那样,一种方法将返回完整的属性,因为它服务于 GetUserFullDetailAndStaffDetail 用例。

    如果您只为所有用例使用一个存储库接口,您将很快意识到它会不断增长,直到包含数十种方法。最后这个界面会变得混乱。有些方法相似,但不同,你会尝试找到疯狂的名字来区分它们

    public interface UserRepository {
     
       public User findSimpleUser();
    
       public User findAllUserInfo();
    
       public User findAllUserInfoForOrderProcess();
    
       // ... maybe dozens more
    }
    

    实现类会很大。也许很多方法是通过私有实用方法耦合的,所以对这个共享代码的更改会影响其他用例,等等。

    分离接口是个好主意。也许您从不同的接口开始,但只有一种实现同时实现了这两种接口。也许您想稍后在情况变得更糟时分解实现。但是你已经有了分离的接口,你不必改变你的用例。

    【讨论】:

    • 关于实体中的ID,我需要将ID放在实体中,因为响​​应体需要该用户ID。实际上,实体中的 ID 本身并没有任何相关的业务规则,它只是充当从 DB 到响应模型的中间数据容器,因为用例无法直接访问 rowResult 数据模型。有更好的解决方案吗?非常感谢您的回答。它让我很清楚。
    • 没关系。我也经常使用数据库 ID,我的很多同事都想要它们。 id 主题超出了这个问题和评论。我想这很难回答。
    • 谢谢,现在更有意义了。将您的答案标记为已接受:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-18
    相关资源
    最近更新 更多