Why Model Objects Shouldn’t Implement Swift’s Decodable or Encodable Protocols

And which objects should

Pablo Manuelli
Sep 1 · 5 min read
Photo by Michael Dziedzic on Unsplash

So far you may be thinking: “What is he talking about? Decodable and Encodable protocols are very useful!”

And I agree with you. The Decodable and 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

The 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 Decodable or 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:

{
"first_name":"dick",
"last_name":"richardson",
"mail":"drichardson@enclave.com",
"day_of_birth":7026198103
}

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 first and last name come within a name field:

{
"name":{
"first":"dick",
"last":"richardson"
},
"email":"drichardson@enclave.com",
"day_of_birth":7026198103
}

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 firstName and lastName, replacing them for name.first and name.last respectively.

We have just changed our domain model due to a change in the data.

That’s the reason I do not use Decodable or 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.

Note that 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 User.

And, finally, a UserDTOMapper creates a new User from de UserDTO data.


Advantages

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 String, Int, or other Decodable types.

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.


Repositories

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.

Example

Let’s see an example:

What is going on here?

  1. 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.
  2. The repository implementation. Unlike the protocol, the name of the class should give us a clue about the persistence store chosen. In this case, APIUserRepository uses an external API to retrieve the Users.
  3. The repository uses URLSession to 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.
  4. A UserDTO struct is used to parse the JSON data obtained from the API. If the data is parsed successfully into the DTO, a UserDTOMapper creates a User from it. If the parsing fails, nil is 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.


Conclusion

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!

Better Programming

Advice for programmers.

Pablo Manuelli

Written by

Technical Owner @ Trivia Crack — Etermax, iOS Software Engineer 🍎

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade