在清洁架构中设计实体的最佳实践是什么?

Sal*_*anz 2 architecture software-design clean-architecture

我正在尝试使用 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
Run Code Online (Sandbox Code Playgroud)
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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

问题 1. 假设如果我有另一个用例(GetUserFullDetailAndStaffDetail 用例)将使用更复杂的 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Ren*_*ink 5

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

该实体是一个域对象,并且 aLoginUser与 a 不同UserDetail。我们常常认为有一个User。但用户有不同的观点。您可以将它们视为一种角色。

public class LoginUser {
  private String name;
  private String email;

  // ...
}
Run Code Online (Sandbox Code Playgroud)

或一个DetailUser.

public class DetailUser {
  private String name;
  private String email;
  private String phone; 
  private String gender;
  // ...
}
Run Code Online (Sandbox Code Playgroud)

id正如你所看到的,我省略了。通常它是数据库详细信息而不是域属性。但有时它是,例如客户编号。

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

它并不违反干燥原则,因为你不会重复自己。我同意我们必须删除重复的代码,但是 aLoginUser会因为不同的原因而改变,然后 a DetailUser。因此它们不会重复。这就是单一责任的含义。

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

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

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

我想说每个用例都应该定义它自己的存储库接口。该接口应该只定义该用例所需的方法。这是接口隔离原则的应用,并且遵循单一责任原则。

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

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

public interface UserRepository {
 
   public User findSimpleUser();

   public User findAllUserInfo();

   public User findAllUserInfoForOrderProcess();

   // ... maybe dozens more
}
Run Code Online (Sandbox Code Playgroud)

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

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