- Assume the remote data store is a generic REST API.
- Assume the save comment endpoint call doesn't have a response object
- Assume all objects (Comment, Annotation, etc.) will follow identical basic CRUD operations (SAVE, GET, DELETE, UPDATE)
I decided to implement a version of "clean" architecture with layers of abstraction. You can imagine four layers:
- Presentation: Contains views and view logic (activities, fragments, viewmodels, presenters, etc.) such as
ScreenViewModel
. - Domain: Contains core object models such as
Comment
as well as the core data interfaceRepository
and any core business logic. *In this particular case, since we're assuming basic CRUD operations, there isn't really any "business logic" to speak of, so we have the presentation calling the data repository directly instead of through usecases/interactors. - Data: Contains implementations of the core data interface such as
CommentRepository
as well as mappers such asCommentApiModelMapper
and serves as an abstraction to lower data sources. - Api: Contains remote data source api classes such as
RestApi
,RestEndpoint
and api models such asCommentApiModel
and theApiModel
superclass.
Since this is Kotlin pseudocode and not a full blown project, I've omited several irrelevant files. That being said, you can probably notice hints of some of the dependencies that might be included in this project based on some annotations and classes. Here's an assumed list to make it clear:
- Dagger2
- OkHttp
- Retrofit
- Gson
- Kotlin coroutines
- Architecture Components (ViewModel)
The project is built with testability in mind with dependency injection for all classes. Some of the Dagger configuration files weren't included for simplicity, but I've thrown in a few modules to make a few dependencies more clear. I usually favor constructor injection whenever I can and default to modules for interface implementations, builders/factories, or "Strategy Pattern" type runtime injection.
By abstracting the project with several layers that have distinct, minimal responsibilities, we gain a lot of flexibility with refactoring and evolving the code. We could rewrite the whole api layer with a different remote host and the rest of the code such as the domain, presentation and even some of the data/repository layer wouldn't have to change. We could introduce a new layer for a local data store to persist data when offline and the only thing that would have to change is the repository implementation.
Each layer is unaware of the implementation details of the layer below.
*One thing to note is that Kotlin itself removes a whole lot of boilerplate code and helps with reducing duplication. It also offers lots of interesting tools like extension functions and sealed classes.
For this solution, I've implemented two different approaches to reduce code duplication and favor reusability.
On the api side of things, I used inheritance to generalize the RestApi
class to accept any ApiModel
. The RestApi
then uses Kotlin's when() {}
to call the correct endpoint. This way, adding a new data class to the project would simply require adding a new implementation of ApiModel
that matches the server's response model and can be serialized properly, and adding an entry to the RestApi
's when {}
operator.
On the data/repository side of things, I used generics to create a single data repository interface Repository
that can handle any and all data types such as Comment
. This approach still has a bit more code duplication since we need to write a new Repository
implementation for every new data class. However, it doesn't require us to perform type checks or casting.
I could've implemented the data/repository layer with a similar inheritance approach to try to reduce code duplication further but I felt like that might clutter the domain layer and its models too much.
We can always push abstraction and DRY principles further but it becomes a question of balancing the pros and cons. In many cases, a bit of repetition can help keep the code clear and readable, which is a huge deal.