CodeX
Published in

CodeX

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…

Name-spacing

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:

Annoying, the 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.

Heterogenous Arrays

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 AnyHashable or 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 AnyInhabiting and AnyHousing are suspiciously similar to the Inhabitant and 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.

Suppose following 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.

Conclusion

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!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Toby O'Connell

Toby O'Connell

Swift / iOS developer - I write about things that I find interesting or innovative.