Search code examples
architecturesoftware-designclean-architecture

What is the best practice for designing entities in Clean Architecture?


I'm trying to implement clean architecture using Kotlin. The flow of the process will be:

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

Code sample:

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
    }
}

Notice that the only attributes used by loginUseCase are user email and password.

Question 1. Suppose if I have another UseCase (GetUserFullDetailAndStaffDetail Usecase) that will use more complex attribute of User, should I use the same UserEntity for GetUserFullDetailAndStaffDetail usecase? So the UserEntity will be:

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)
    }
}

If I use different entity it will violate DRY principle (duplicate UserEntity and duplicate getUser method in UserDataStoreImplementation), but if I use the same UserEntity for GetUserFullDetailAndStaffDetail usecase, the getUser in UserDataStoreImplementation for LoginUseCase must get full attribute which is useless.

Question 2. Should there be different methods for getUser in UserDataStoreImplementation (one will return partial attribute in UserTable for LoginUseCase, the other will return full attribute in UserTable for GetUserFullDetailAndStaffDetail UseCase)?


Solution

  • Question 1. Suppose if I have another UseCase (GetUserFullDetailAndStaffDetail Usecase) that will use more complex attribute of User, should I use the same UserEntity for GetUserFullDetailAndStaffDetail usecase?

    The entity is a domain object and a LoginUser is not the same as a UserDetail. We often think that there is one User. But there are different perspectives of a user. You can think of them as a kind of roles.

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

    or a DetailUser.

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

    I omitted the id as you can see. Usually it is a database detail and not a domain property. But sometimes it is, e.g. like a customer number.

    If I use different entity it will violate DRY principle (duplicate UserEntity and duplicate getUser method in UserDataStoreImplementation), but if I use the same UserEntity for GetUserFullDetailAndStaffDetail usecase, the getUser in UserDataStoreImplementation for LoginUseCase must get full attribute which is useless.

    It doesn't violate the dry principle, because you don't repeat yourself. I agree that we have to get rid of duplicated code, but a LoginUser will change for different reasons then a DetailUser. Thus they are not duplicated. That's what the single responsibility is about.

    They look similar, but similarity is only a hint for duplication. You have to ask yourself more questions to find out if they are really duplicated. Let's think about a change to the login use case. Maybe only the name should be displayed. Then the two entities will only have 1 property in common - the name. How many properties must they have in commen to be duplicated?

    If you implement business logic in the entities you will realize that there are methods that will only be called in one of the two use cases and that these methods use only a subset of the properties. Then you will see that these two entities are different.

    Question 2. Should there be different methods for getUser in UserDataStoreImplementation (one will return partial attribute in UserTable for LoginUseCase, the other will return full attribute in UserTable for GetUserFullDetailAndStaffDetail UseCase)?

    I would say that each use case should define it's own repository interface. This interface should only define the methods that this use case needs. This is an application of the interface segregation principle and it honors the single responsibility principle.

    Like you pointed out the one method will return the full attributes, because it serves the GetUserFullDetailAndStaffDetail use case.

    If you only use one repository interface for all use cases, you will quickly realize that it will grow and grow until it contains dozens of methods. Finally this one interface will become confusing. Some methods are similar, but different and you will try to find crazy names to distinguish them.

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

    The implementation class will be large. Maybe a lot of methods are coupled through private utility methods, so that a change to this shared code affects other use cases, and so on.

    It is a good idea to separate the interfaces. Maybe you start with different interfaces but only one implementations that implements both. Maybe you want to break up the implementation later when it get's worse. But then you already have the separated interfaces and you don't have to change your use cases.