Terminal prompt

iOS Developer | Release Engineer | Mac IT Engineer

Using Data Transfer Models to Remove Strong Coupling

Consider the following dependency diagram:

Dependency diagram illustrating the relationships between Weather API Module, Weather Feature Module, and Weather Cache Module, highlighting the protocols and types involved.

For those less familiar with these diagrams, solid lines with solid arrow heads indicate source code dependencies, and dotted lines with open arrows indicate types that implement a protocol.

In this little app, we have three modules:

  • Weather Feature Module
  • Weather API Module
  • Weather Cache Module

Tightly Coupled Protocols

In the Weather Cache Module, we have a Protocol <WeatherStore> with a source code dependency on the WeatherReport type in a different module.

This presents a problem. Any type that implements the WeatherStore protocol will then also have a source code dependency on WeatherReport. If WeatherReport ever needs to change, we could be breaking any number of implementations of that protocol.

This kind of coupling can quickly become a major bottleneck. Let’s look at one way to handle this.

Data Transfer Objects to the Rescue

To decouple the WeatherStore protocol from the WeatherReport type, we can use a data transfer object, or a DTO.

Data Transfer Objects are typically simple types with no business logic. It’s just a structured data container that defines only the information that should be shared between different parts of your application.

We use Data Transfer Objects to create clear boundaries between our internal domain models and the data we expose externally.

To understand this better, let’s use one to decouple our code.

Decoupling WeatherStore With a DTO

Let’s get into the example:

Code snippet outlining the WeatherStore protocol, WeatherReport struct, and LocalWeatherLoader class for managing weather data caching.

This WeatherStore has an insert(:::) function that inserts an array of WeatherReport‘s into a local cache. This cache might use a variety of frameworks for persistence, such as SQLite, json files, etc. As it currently stands, each of those implementations would need to use the same WeatherReport model. This could easily cause problems. For example, the existing WeatherReport represents the data from the API response. But when we want to cache a WeatherReport, there might be other data we want to store in the model, such as a timestamp or file location. But those properties would only be relevant to a specific caching implementation.

In other words, we really have two different “WeatherReport” models:

  • The API’s model
  • The local cache’s model

This is the kind of problem that a Data Transfer Object was designed to solve. Let’s implement one and see how comes together.

First, we can essentially make a copy of the existing WeatherReport model, and then update the WeatherStore to reference that model instead:

Code snippet displaying the definition of the WeatherStore protocol, including methods for managing cached weather reports and the LocalWeatherReport struct with properties for id, summary, temperature, and date.

Since the LocalWeatherLoader calls the updated .insert(:::) method on the WeatherStore, we need to update that as well. The cache(::) function on LocalWeatherLoader accepts an array of WeatherReport‘s and inserts them into the WeatherStore instance. Now that this instance accepts only LocalWeatherReport models, we need a way to map [WeatherStore] --> [LocalWeatherStore]:

Code snippet showing a private extension on an array where the element is WeatherReport, defining a toLocal method that maps WeatherReport instances to LocalWeatherReport instances.

We create a private extension on Array, scoped specifically to arrays where the elements are WeatherReport. In that extension, we define a toLocal() function that just maps the array to an array of LocalWeatherReport‘s.

Then we update the cache(::) function to use that new function:

Code snippet of the LocalWeatherLoader class, featuring a cache function that inserts WeatherReport instances into a WeatherStore.

Here’s how the change looks in our dependency diagram:

A dependency diagram illustrating the relationships between the Weather API Module, Weather Feature Module, and Weather Cache Module, detailing classes like RemoteWeatherLoader, LocalWeatherLoader, WeatherStore, WeatherReport, and LocalWeatherReport.

Wrap Up

Strong coupling can be hard to notice. So dependency diagrams can be a useful tool for finding couplings that can become bottlenecks.

Data Transfer Objects can be a useful tool when separate parts of your software depend on a shared model. But they aren’t always the right tool for the job. In a simple codebase, especially when it does nothing more than consume and display data from an external API, using a single shared model can work quite well.

But as soon as your app needs to do more, using Data Transfer Objects to decentralize the use of models can be a powerful tool for making your codebase more modular, more flexible, and more resilient to changes.

,

Leave a Reply

Discover more from montgomery.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading