I came across this post (and more like it) claiming extensions to be a good, or at least different, solution for mapping DTO's.
Are they though? Aren't DTO's supposed to be pure data objects? I've always been taught to seperate my mappings in special mapping services or mapping libraries like MapStruct and ModelMapper for implementing the good practice of "seperation of concerns".
Are they though? Aren’t DTO’s supposed to be pure data objects? [...] So what about extensions?
I'm not 100% familiar with Kotlin, but I think extension methods in Kotlin are the same as C#, basically syntactic sugar.
So you'd write an extension method like:
public static class ObjectExtension
{
public static void DoSomething(this object input) { }
}
this being the keyword making it an extension method - and you can call that function on an object by doing object.DoSomething() - Yes. But underneath it's the same as doing ObjectExtension.DoSomething(object) (A static invocation to your ObjectExtension class and DoSomething method.
So on the surface it looks like you're injecting a new method into your DTO, and your DTO is not a pure data object anymore, but actually you're just creating an helper function that's statically invoked - that looks like you can call it "on the object" but actually you're not.
As for whether it's a good / common practice to create mappers like that in Kotlin, I don't really know. I do it often in C# though.
I was suspecting something like that. I'll be looking into it a bit more.
I also notice some difference in what's considered "best practice" between Java and C#, so I'm not sure what's actually best practice haha.
Probably somewhere in the middle, I suppose.
I think it boils down to where you define the extension functions and how that impacts coupling.
At some level you want to divorce the repository storage of the data from your domain object. Let's say that the repository changes, and "name" is no longer just "name", but now "firstName" and "lastName". The body of your application doesn't care, or need to know that the repository has changed, as it will still just deal with a name, whatever that is.
So something has to put "firstName" and "lastName" together into a "name", and it needs to be consistent with how the application has always received it. Is it "Fred Smith", "Fred, Smith" or "F. Smith"? And who "owns" that logic?
From a coupling perspective, you don't want the application logic to know anything about the repository or the internal structure of the DTO. On the other hand, you don't want the repository service layer to know about how the data is going to be used.
Let's say that you have two different applications that used the "name" field, but in different ways somehow. So the conversion from the two "name" fields into one might be different for each application. Yes, you could argue that recombining them back exactly the way the repository service originally delivered "name" would be transparent to the client applications, but what if the change to the repository was driven by one of those applications needing split data?
That's usually why you put your adapters in some neutral place, associated with the client application but yet somewhat outside of it.
You could use extension functions to provide the adapter, but you need to make sure that they're not co-mingled with you application code. Otherwise you've just reestablished the coupling between the repository and the application that you where trying to avoid.