Right place to map to Domain in Android clean architecture
Asked Answered
U

3

11

Me and my colleague are having a debate as to where would be the right place to map our entity objects or remote dto objects to plain simple domain objects.

Our structure looks like this.

source(includes dao) > repo(includes source) > usecase(includes repo)

My colleague thinks that mapping to domain should be done inside the source so that the domain object could be passed on to the next layers as so

class SomeSourceImpl(private val dao: Dao) : SomeSource {
    override fun get(): Observable<DomainModel> {
        return dao.getResponse().map { it.mapToDomain() }
    }
}

My colleagues argues that according to Uncle Bob this is due to the dependency rule.

This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in the an inner circle. That includes, functions, classes. variables, or any other named software entity.

I very much disagree with the approach of mapping to domain directly inside the source because then the repositories become anaemic and we are consequently adopting the anti-pattern of anaemic repositories being useless and all they do is to blindly propagating everything that comes from the source. (Now you may say that sources are also anaemic and we could simply remove them and include the dao object directly into the repo but this is out of the question in our case).

Instead I propose that sources would return the raw database entity (or remote entity if we are into rest calls) as it makes sense for a source to return the raw data for later processing. It's the job of the repo to get the result from the source then map it to domain and lastly propagate this domain object to use cases something like so.

class SomeRepoImpl(private val someSource: SomeSource) : SomeRepo {
    override fun get(haId: String): Observable<DomainModel> {
        return otherAssetSource.get().map { it.mapToDomain() }
    }

I also came across some samples on github where they map to domain inside their repos rather than the sources

Here

Here

Here

Here is one for iOS too

What would be the strict rule in clean architecture principles regarding the place one can map an entity into a domain object?

Unquestioning answered 10/12, 2021 at 22:34 Comment(2)
Lower layers cannot depend on code from a higher layer, so map to a domain in the domain layer. "Nothing in an inner circle can know anything at all about something in an outer circle." source is the inner circle and domain is the outer.Goddamned
I checked numerous repos too and from what I observed mapper class resides at data layer while mapping is done in repositories concrete implementation which also resides in data layer. Thus in data layer you will have local, remote, mapper, and repositories folder.Straightforward
S
9

Quoting the rule

source code dependencies can only point inwards

That would depend on the architecture I guess. Let me explain this with an example:

Architecture:

DOMAIN <- DATA <- PRESENTATION

Where:

DATA -> LOCAL  
|  
v  
REMOTE

NOTE: DOMAIN represents the innermost circle and PRESENTATION represents the outmost circle.

Now DOMAIN is a pure Kotlin module and does not have any Android dependencies. Let's define a repository:

interface ProfileRepository {
    
    fun getProfile(): Profile?

    fun updateProfile(profile: Profile): Profile
}

We implement this in the DATA layer(which is an Android library):

class ProfileRepositoryImpl(
    private val networkManager: NetworkManager,
    private val remoteDataSource: ProfileRemoteDataSource,
    private val localDataSource: ProfileLocalDataSource
): ProfileRepository {
     
    override fun getProfile(): Profile? {
        return if(networkManager.isNetworkAvailable) {
            localDataSource.insert(remoteDataSource.get())
        } else {
            localDataSource.get()
        }
    }

    override fun updateProfile(profile: Profile): Profile {
        val updatedProfile = remoteDataSource.update(profile)
        return localDataSource.insert(updatedProfile)
    }
}
class ProfileRemoteDataSource(
    private val api: ProfileApi,
    private val mapper: Mapper<ProfileDto, Profile>
) {
   
    fun get(): Profile {
        return mapper.toModel(api.getProfile())
    }

    fun update(profile: Profile): Profile {
        val dto = api.updateProfile(
            mapper.fromModel(profile)
        )
        return mapper.toModel(dto)
    }
}
class ProfileLocalDataSource(
    private val dao: ProfileDao,
    private val mapper: Mapper<ProfileEntity, Profile>
) {

    fun insert(profile: Profile): Profile {
        dao.insert(mapper.fromModel(profile))
        return requireNotNull(get())
    }

    fun get(): Profile? {
        return dao.get()?.let(mapper::toModel)
    }
}
interface Mapper<T : Any, Model : Any> {
    fun toModel(value: T): Model
    fun fromModel(value: Model): T
}

The LOCAL module is an Android library independent of any dependencies and exposes the DAO and Entity objects:

interface ProfileDao {
    fun insert(profile: ProfileEntity)
    fun get(): ProfileEntity?
}

Similarly, for the REMOTE module:

interface ProfileApi {
    fun get(): ProfileDto
    fun update(profile: ProfileDto): ProfileDto
}

So, it doesn't make sense for me to have the Source classes return DTO and Entity objects. The repo class would look something like this:

class ProfileRepositoryImpl(
    private val networkManager: NetworkManager,
    private val remoteDataSource: ProfileRemoteDataSource,
    private val remoteDataMapper: Mapper<ProfileDto, Profile>,
    private val localDataSource: ProfileLocalDataSource,
    private val localDataMapper: Mapper<ProfileEntity, Profile>
) : ProfileRepository {

    override fun getProfile(): Profile? {
        if (networkManager.isNetworkAvailable) {
            val dto = remoteDataSource.get()
            val profile = remoteDataMapper.toModel(dto)
            val entity = localDataMapper.fromModel(profile)

            localDataSource.insert(entity)
        }

        return localDataSource.get()?.let(localDataMapper::toModel)
    }

    override fun updateProfile(profile: Profile): Profile {
        val request = remoteDataMapper.fromModel(profile)
        val dto = remoteDataSource.update(request)
        
        val updatedProfile = remoteDataMapper.toModel(dto)
        
        val entity = localDataMapper.fromModel(updatedProfile)
        localDataSource.insert(entity)

        return localDataMapper.toModel(
            requireNotNull(localDataSource.get())
        )
    }
}

In your example, you have taken only the GET operation into consideration. Here, for the UPDATE operation we need to map the DOMAIN object as well. So as we add more functionalities the Repo class would become very messy if the mapping of objects is done in the Repo class.

I believe it would depend on the overall architecture of the system.

Skirling answered 11/12, 2021 at 20:22 Comment(3)
what is the purpose of the repository if fetching, error handling and mapping are all done in the datasource?Dependent
@Dependent In my case, the purpose is to handle data from various sources. Moreover in most of my use-cases certain data sources are used in multiple repositories so instead of mapping in multiple repositories I resort to so in the data source.Skirling
@Dependent i will give my point of view on top of Xid's comment. The repository acts as a mediator for the multiple datasources defined in it, aka it handles the data from those datasources as said in the comment above. By doing certain thinks/task such as mapping on the right place makes your code maintanable and testable.Drue
S
3

In the context of Android, the developer doc on Guide to app architecture has this diagram with the given footnote. The arrows represent dependencies between classes. The domain layer depends on data layer classes. The lower/inner layer cannot know anything about the higher/outer layer. This would mean that the mappers cannot be in the lower/inner layer. Doing so would put a reference to the domain model in the data layer; thereby breaking the dependency rule.

Hence, in the android context, the domain being the higher layer should hold the mappers.

enter image description here

Schoolmarm answered 16/10, 2023 at 18:16 Comment(0)
D
1

I am learning the subject, but my approach is on Clean Architecture which I use in most projects.

The Domain Layer is the innermost layer, therefore, it does not depend on other layers. Thus, the first decision is to make mappers stay in the data and presentation layers, or whichever layers I use for the two purposes.

Then, I have an interface that defines how I define my mappers

interface EntityMapper<M : Model, ME : ModelEntity> {
    fun mapToDomain(entity: ME): M

    fun mapToEntity(model: M): ME
}

Then, I have classes, in the data layer which map from data models to domain model. An example is

class ItemEntityMapper @Inject constructor(
    private val ownerEntityMapper: OwnerEntityMapper
) : EntityMapper<Item, ItemEntity> {
    override fun mapToDomain(entity: ItemEntity) = Item(
        id = entity.id,
        name = entity.name,
        fullName = entity.fullName,
        description = entity.description,
        url = entity.url,
        stars = entity.stars,
        owner = entity.ownerEntity?.let { ownerEntityMapper.mapToDomain(it) }
    )

    override fun mapToEntity(model: Item) = ItemEntity(
        id = model.id,
        name = model.name,
        fullName = model.fullName,
        description = model.description,
        url = model.url,
        stars = model.stars,
        ownerEntity = model.owner?.let { ownerEntityMapper.mapToEntity(it) }
    )
}

I prefer OOP classes over functional Kotlin to ease with DI

Disassemble answered 22/11, 2022 at 20:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.