Reputation: 5859
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 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?
Upvotes: 11
Views: 6643
Reputation: 2451
The official Android architecture is different from other architectures, such as "Clean Architecture": Now-in-Android app architecture journey
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.
NOTE: This is a hotly contested topic. There is a discussion regarding this on the now-in-android app repo. You can read it here: Breaking SOLID principles and Clean Architecture.
Upvotes: 3
Reputation: 11
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
Upvotes: 1
Reputation: 4951
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.
Upvotes: 9