Why Model Objects Shouldn’t Implement Swift’s Decodable or Encodable Protocols
And which objects should
So far you may be thinking: “What is he talking about?
Encodable protocols are very useful!”
And I agree with you. The
Encodable protocols are very useful indeed. It is great that Swift provides a native way to parse JSON elements or to store and retrieve objects from User Defaults, for example. There is nothing wrong with it.
But, I think that we are making a mistake by using these protocols in our model objects. And I will try to explain why.
Domain Model and Data Model
The domain model is an object‐oriented model that incorporates both behavior and data. It’s a representation of the business rules that we are trying to model.
The data model is the structure of the data in a persistence store. It has no behavior.
Some examples of persistence stores are User Defaults, Core Data, a file, a database, or even an external API. The data model could be different in each of these stores.
Both the domain model and the data model contain data but the domain model also contains the business rules.
The objects in the domain model should be ignorant about which persistence store or data model is used.
That’s because the domain model and data model have different reasons to change. The domain model should change only when business rules do or when more insight about the problem to solve is gained.
On the other hand, the data model may change for different reasons. For example, the persistence store needs to change from a local store to a remote API. The domain model should not be affected by this infrastructural change.
Decodable and Encodable
Decodable protocol is used to hydrate objects from some external representation. For example, it is used to parse JSON objects into structs or classes.
Decodable: A type that can decode itself from an external representation.
On the other hand, the
Encodable protocol is used to store objects to some external representation. For example, it can be used to obtain a JSON representation of an object.
Encodable: A type that can decode itself to an external representation.
But why shouldn’t we use
Encodable in our domain model objects?
Let’s work with an example to answer that question. Assume we have the following JSON representation of a user:
And we use a
Decodable struct, named
User, to both parse the JSON and represent a
User in our domain model:
But what happens if the JSON changes? Let’s say that now the
last name come within a
Due to this small change, the previous
User struct now fails to parse the JSON data. We are forced to change the domain model to parse the new data model:
Good. Now the
User struct parses the new JSON format but we have to change all the uses of
lastName, replacing them for
We have just changed our domain model due to a change in the data.
That’s the reason I do not use
Encodable in my domain model objects.
Separate Domain Model From Data Model
What we need to do is decouple the domain model from the data model.
We can achieve this by using two different classes or structs. One that parses the JSON and another that represents the domain model object.
User does not implement the
Decodable protocol anymore, because it’s not used to parse the JSON data.
User now represents the domain model and is decoupled from the data model.
We have created a
Decodable struct named
UserDTO (Data Transfer Object) which is used to parse the JSON data. This struct contains the data needed to create a
And, finally, a
UserDTOMapper creates a new
User from de
Due to this approach, the domain model is no longer coupled to the data and it doesn’t need to change every time the data model does.
Of course, the domain model is not immune to all data changes. Sometimes, the model will change anyway.
In that case, ask yourself: “Is it the data that forces the domain model to change or is it the other way around?” Maybe a business rule changed and the domain model has changed, which leads to a data model change.
Another advantage of decoupling the domain model from the data is that the domain model becomes more expressive. We can use more complex types rather than just plain
Int, or other
In the previous example, the date of birth is now represented as a
Date in the
User struct, unlike the
UserDTO struct where the date of birth is represented as an
Int. Those more complex types can be created in the mapping process.
Now that we know the value of decoupling the domain model from the data, I want to introduce a concept that can help us archive that goal: The repository.
A repository can be seen as a collection of elements where they can be stored or retrieved. It provides methods to obtain those elements and store them.
It’s a boundary between the domain model and the data model. It’s a good place to hide the real persistence store used and all its implementation details, like JSON parsing and mapping to domain model objects.
Let’s see an example:
What is going on here?
- The repository protocol. It’s a good idea to work with protocols because this way, the real repository implementation can be changed very easily using dependency injection. It’s named
UserRepository. The name shouldn’t tell us anything about the persistence store used.
- The repository implementation. Unlike the protocol, the name of the class should give us a clue about the persistence store chosen. In this case,
APIUserRepositoryuses an external API to retrieve the
- The repository uses
URLSessionto perform the request and obtain a
User. I’m not going to dig into more detail here because I don’t want to miss the point of the example. If you want to know more about networking using
URLSession, you can see a very good tutorial here.
UserDTOstruct is used to parse the JSON data obtained from the API. If the data is parsed successfully into the DTO, a
Userfrom it. If the parsing fails,
nilis returned instead.
And that’s all. Very simple, right?
When you work with repositories, it’s quite easy to change the persistence store used. Let’s do that and store the users locally. This new implementation, as its name suggests, uses User Defaults to retrieve users:
Note that the DTO used in this implementation is the same one used in
APIUserRepository. This, of course, is not mandatory.
Each implementation can use a different DTO that suits the needs of the repository. But to keep the example simple, I used the same one.
A good thing about repositories is that you can hide all the implementation details inside of it, behind a protocol.
The object that consumes the repository shouldn’t care which mechanism is really used. All that it cares about is that the repository returns a domain model object, a
User in this example.
And, as we have decoupled the domain model from the data model, the repository implementation can change with minimum impact on the system because the domain model returned will stay the same.
Thanks for reading!