https://kotlinlang.org logo
Title
k

Kamila

04/04/2022, 7:19 AM
Hi guys, topic of transforming domain objects into Dto (or enything else), I know I can do something like:
class Something(val name: String) {
    companion object {
        fun toDto(): Dto(name)
    }
}

data class Dto(val name: String)
But I would prefer to have possibility to write:
Dto.from(foo: Something) = Dto(name = foo.name)
Is it possible?
k

kqr

04/04/2022, 7:24 AM
you mean you don't want companion object in Dto?
k

Kamila

04/04/2022, 7:25 AM
I would prefer to avoid companion object on Dto, if possible
k

kqr

04/04/2022, 7:29 AM
fun Something.toDto() = Dto(this.name)
?
k

Kamila

04/04/2022, 7:30 AM
But this looks like I am adding some functionality to the domain object, which at first glance looks like breaking the principles
especially if there are multiple external systems and each maps
Something
into different Dto
k

kqr

04/04/2022, 7:35 AM
well I would go with companion object. maybe take a look at https://kt.academy/article/ek-factory-functions
k

Kamila

04/04/2022, 7:45 AM
Companion object on the
Dto
or
Something
k

kqr

04/04/2022, 7:47 AM
Dto
domain should not be bothered with "infra"
j

Joffrey

04/04/2022, 7:54 AM
You can also declare extensions on the companion but outside the class itself:
fun Dto.Companion.from(..)
k

Kamila

04/04/2022, 7:58 AM
The problem may be when having multimodule app, whith mediator module that would bind together Domain (containing domain classes) and API (containing dtoS). API will not know about domain, thus would not be possible to add mappiing from domain to dto inside of companion object of DTO
j

Joffrey

04/04/2022, 8:02 AM
The point was to extend the companion object of the Dto class but not declare that function in the class (so you can declare this outside the module)
k

Kamila

04/04/2022, 8:09 AM
aha!
k

kqr

04/04/2022, 8:11 AM
in mediator you also have to have some contract right?
k

Kamila

04/04/2022, 8:11 AM
right
k

kqr

04/04/2022, 8:12 AM
and it either can be domain or some 3rd object, right?
anyway, I would avoid having domain object to know about outer layer objects
so there have to be some shell around it doing conversions
k

Kamila

04/04/2022, 8:16 AM
Correct, yes
@Joffrey I like the idea of extending companion object, but does it mean I have to specify
companion object {}
on the DTO? Or is there some magic to be able to do
data class Dto(val name: String)

fun Dto.Companion.from(smth: Something) = Dto(name = smth.name)

// Or do I have to write 
data class Dto(val name: String) {
    companion object { }
}
j

Joffrey

04/04/2022, 8:26 AM
I remember having to write an empty companion at some point, but I thought they intended to remove this constraint. Maybe it's still necessary 😕 I also don't like those empty companions to be frank. If it's still necessary, I'd rather go for the regular extension the other way around without companion:
fun Domain.toDto(): Dto
k

Kamila

04/04/2022, 9:54 AM
This actually seems to be a convention in Kotlin, when checking Boolean implementation (there is an empty companion object too)
So, I landed on a file with top-level functions to map from domain to dto, where these functions are extension of type
fun Dto.Companion.from(obj: Domain) = Dto()
j

Joffrey

04/04/2022, 11:29 AM
This begs the question though, why do you dislike the
toDto()
version so much?
k

Kamila

04/05/2022, 8:32 AM
The reason is the domain object in this particular case is being translated into multiple different DTOs. In consequence, having:
dto: SomeDto = domain.toDto()
dto: SomeOtherDto = domain.toDto()
dto: YetAnotherDto = domina.toDto()
which reads worse than
dto = SomeDto.from(domain)
dto = SomeOtherDto.from(domain)
dto = YetAnotherDto.from(domain)
j

Joffrey

04/05/2022, 8:36 AM
That would look really nice to me:
val dto = domain.toSomeDto()
val dto = domain.toSomeOtherDto()
val dto = domain.toYetAnotherDto()
It goes in line with Kotlin's
String.toInt()
and the likes. And it also reads nicer (IMO) when chained, especially with nullable values:
val dto = something.getSomeDomainEntitySomehow()?.toSomeDto()
As opposed to:
val dto = someting.getSomeDomainEntitySomehow()?.let { SomeDto.from(it) }
k

Kamila

04/05/2022, 8:50 AM
Yes, those are good points, but some of those transformation may need additional information, which would made it
domain.toSomeDto(other: Info)
vs
SomeDto.from(domain, other)
which IMHO reads easier in the way of understanding how
someDto
composed
j

Joffrey

04/05/2022, 8:55 AM
If the resulting DTO is an aggregation of data from the domain and from the extra params, and the other info is as important as the one from the domain, then I can see your point. The factory function might be better in this case. However, if the extra info is just a side thing, or information about how to transform from domain to DTO, I would still go for the
toDto(extra info)
variant, like
toInt(radix)
. It always boils down to the message you want to convey, I'll let you judge of that 🙂
k

Kamila

04/05/2022, 8:40 PM
That was very nice discussion and interesting points mentioned, thank you 🙂
👍 1