#
PurposeThe purpose of the Converter pattern is to provide a generic, common way of bidirectional conversion between corresponding types, allowing a clean implementation in which the types do not need to be aware of each other.
In plain words, the Converter pattern makes it easy to map instances of one class into instances of another class.
We're making use of Converter to map models when they are propagating between layers (API, Data, Domain, Presentation) to make them convenient to work with on a specific layer and do not spread extra dependencies to other layers.
#
DIPConversion is here to comply with dependency inversion principle. Domain layer only know about itself. UI layer needs to convert UI model into Domain DTO to deal with Domain layer. Data layer needs to convert Data model into Domain dto to deals with Domain layer.
#
SRPWe want to extract conversion methods to comply with Single Responsibility Principle. Putting conversion logic in its own scope, makes it easier to test. Extracting conversion logic from other classes, ensure that they focus on their business and not in conversion logic.
#
ImplementationActually, there are different uses cases that we should handle that would trigger different implementations
#
Simple extension methodFor the simpler cases, it is advised to stick with YAGNI principle and have only an extension method in a file named <source type>To<destination type>Converter.kt
because you aren’t gonna need a class.
fun MyModel.toMyOtherModel() = MyOtherModel( attr1 = id, attr2 = name, //...)
But I need to handle lists of items !
Kotlin is here to help thanks to the
map{}
extension to collection classes
// Given this converter methodfun MyModel.toMyOtherModel(): MyOtherModel {}// And the fact that we have a list of objects to convertval myModels = listOf(myModel1, myModel2, myModel3)
// Convertion could be done with map extension method.val myOtherModels = myModels.map { it.toMyOtherModel() }
❌ Do not call extension method from domain layer.
#
Simple classYou can create a converter class if you want to use Injection features or if you need additional contributors. You should always avoid to use kotlin objects
because using a Singleton is often a code smell. It is a ressource killer because never destoyed. They are hiding dependencies and states and they are making your application more coupled.
#
Example👉 Create the converter class that can be injected thanks to @Inject
class ATypeToAnotherTypeConverter @Inject constructor() fun convert(from: AType): AnotherType = when (this) { AType.A-> AnotherType.A AType.B-> AnotherType.B }}
#
We need an additional contributorSometimes you’ll need an additional contributor like a LocaleProvider
or a DateProvider
👉 Inject them in the converter class
class DomainDtoToUiModel @Inject constructor( // here timezone is dynamic, it is retrieved from a provider timeZoneProvider: TimeZoneProvider) {
// Here date format is static. To make is dynamic, format can be injected in constructor. private val simpleDateFormat = SimpleDateFormat(/*...*/)
fun convert(dto: DomainDto): UiModel { return UiModel( displayDate = simpleDateFormat.format(dto.currentDate, timeZoneProvider.getLocalTimeZone()) ) }{
#
We need a converter with additional parametersMost of the time, it is an unexpected case. Converters have 1:1 relationship.
💡 If logic begins to be complex, or you’re creating several implementations of an interface, you should consider using a Factory or a Builder instead.
You might want to convert a domain DTO in a model of UI or data layer, then use the last model along a factory to create more complex instance.
#
âś…Â Do- Place converter in UI or Data layer (along ViewModel or DataSource)
- Make converter as simple as possible
#
❌ Don’t- 🧨 Conversion method can crash
- Call converter from Domain layer
- Implement complex logic
- Extend very common classes like
String
,Long
orInt
#
Naming#
Class<source type>To<destination type>Converter
, e.g.:
LocalCardToCardConverter
(LocalCard 👉 Card)RequestToLocalRequestConverter
(Request 👉 LocalRequest)
đź’ˇ It should be clear what types converters are converting. This is the reason why the source type and the destination type are mentioned.
#
Extension methodThe extension method used for converting the extended object into the target one should be named : fun Model.toNameOfTargetedClass(){}
It should belong to a file named <source type>To<destination type>Converter.kt
đź’ˇ The destination type of the converter extension method should be crystal clear. A model can also be converted in several kind of classes. This is the reason why methods like .toDomainModel()
or .toDataModel()
are not encouraged.
Given this data classes:
// Data layerdata class RemoteTypeOfMyObject()
// Domain layerdata class MyAwesomeData()
// UI layerdata class AwesomeUiState()
Then we should have these extension methods
// Data layerfun RemoteTypeOfMyObject.toMyAwsomeData(): MyAwesomeDatafun MyAwesomeData.toRemoteTypeOfMyObject(): RemoteTypeOfMyObject
// UI Layerfun AwsomeUiState.toMyAwsomeData(): MyAwesomeDatafun MyAwesomeData.toAwesomeUiState(): AwesomeUiState
#
FileConverter class or methods should be placed into their belonging layer, not in the domain one.
⚠️ Domain layer should not know about classes that are in UI or in Data layer.
root |- Data | |-...- RemoteTypeOfMyObject.kt | |-...- RemoteTypeOfMyObjectConverter.kt <- Converter is here | |- Domain | /!\ No converters here /!\ | |- UI |-...- AwsomeUiState.kt |-...- AwsomeUiStateConverter.kt <- Converter is here
#
Layers#
DataConverters in data layer should be called from data sources. Repositories are dealing with business models.
A source is part of the external world : everything that you don’t own and that you want to scope into the smallest possible part. No-one should know about your source implementation details except the source itself. So it should be responsible to expose a model that the consumer can undestand without having to know the implementation details of that source
#
UIConverters in UI layer should be called from ViewModels. They are the bridge between UI model and business rules (implemented in UseCases)