Why structs are better than protocols for dependency inversion
When we think of dependency inversion in Swift, protocols are usually the first thing that come to mind. The well known pattern is to create a protocol that defines some required behaviour and create a concrete implementation that performs that responsibility.
This becomes invaluable when testing our code as we can substitute dependencies for mocked versions which can be primed with desired responses and queried for expected outcomes.
Whilst this pattern is perfectly adequate in most instances, structs can often provide advantages that protocols can not.
Where protocols fall down…
Once major disadvantage of protocols is that they cannot nest other types. Imagine the following snippet:
If we tried to define a protocol for our
House, we would also need to define a protocol for the
Inhabitant too, like so:
Inhabiting protocol cannot be nested inside
Housing, so it pollutes the global namespace. Over time, this can cause conflicts and cashes, especially in large codebases.
Suppose our consuming type required an array of houses. With the following snippet we run into the infamous
Protocol ‘Housing’ can only be used as a generic constraint because it has Self or associated type requirements error.
Here we have 2 options. The first is to make
Street generic, like so:
The problem here, however, is that now we are tied to one particular type of Housing. On our Street we might have multiple types of house like Bungalows and Mansions so this might not always be what we want.
The other solution is to create a type-erasing wrapper in a similar way to how
AnyPublisher work. Here we could create the type-erased
AnyHousing, like so:
But oh no! Here we can see that we’ve tied ourselves to one specific type of Inhabitant per house! To fully implement the type erase we need an
AnyInhabiting type too. The final result is as follows:
That’s pretty verbose and if we look closely, the implementations of
AnyHousing are suspiciously similar to the
House we started off with!
The trick to using structs…
The answer to how we can use structs to perform the same function as protocols is by keeping them simple! We can write the struct in such a way that it holds only the data and functionality required by the consuming type. Instead of conforming to a protocol we can write an adaptor to map any object to our struct.
ViewModel. It allows us to define the text and
buttonTapped functionality upon initialisation.
We can now add a convenience initialiser in an extension to map from the services we need to call upon.
When it comes to testing our UI layer, there is no need to create an object and conform to a protocol. We can simply provide test functionality and text upon initialisation.
We’ve now seen an alternative to protocols that performs the same function, maintains the same level of testability and simplifies verbose boilerplate code. Seems too good to be true right?
The one caveat I have seen is that some discipline needs to be applied to the initial struct in keeping it as a data-only object (Kotlin style data-classes would be ideal here).
I find this pattern cleaner and less verbose in most cases when compared to protocols, so it continues to be something I use in my daily work
For completeness, here is a gist which continues the House example in the same way!