Dependency Inversion Principle in Swift

Rodrigo Maximo
Movile Tech

--

Introduction

If you are a developer and you are interested in Software Engineering, you have probably already heard about a concept called SOLID.

In case you have never heard about it, do not worry, the first article of this serie has an introduction here.

Dependency Inversion Principle (DIP)

In the two previous articles, we’ve discussed about two principles, OCP (Open Closed Principle) and LSP (Liskov Substitution Principle). In order to remember, the OCP states every code structure should be opened to extension, but closed to modifications, while the LSP states we shouldn’t have difference in terms of behaviors when substituting a class by any of its subclasses.

We’ve also seen those two principles can be co-related (Please take a look in the LSP article’s second example to remember about it).

But after all, why are we telling about those two different principles instead of mentioning the principle this article is about? Because the Dependency Inversion Principle (DIP) are resulted from rigorous use of those two principles, as we’re going to see during this article. But first, let’s bring some important definitions.

Dependency Inversion Principle

According to Uncle Bob’s paper, this principle says that:

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

Bad Design

Before getting more details about the DIP, let’s briefly talk about something that can be commonly heard in the developer’s world: Bad Design.

A bad design is a condition attributed to a software, or sometimes even to a whole project, when it was not designed in a good way. Sometimes, it was designed in a good way, but, with time and people working on it, it became a bad designed software. Here it’s important to emphasize it can be precipitated to consider a software a bad designed one just because you’d do differently, or because you don’t agree with some decisions were made during its conception.

So, how can we classify a software as well designed?

It can be audacious to define how to know if a software has a bad design. However, probably most of developers and software engineers would agree if a software, although it behaviors as it should, it does have some of those characteristics, it can be classified as a bad designed one:

  • Rigidity: It’s hard to change it, because possible changes affects this software’s others parts.
  • Fragility: It’s difficult to implement modifications, because possible changes breaks this software’s others parts.
  • Immobility or Coupling: It’s really difficult to reuse or extract some piece of code because everything is totally coupled, and if you need to do it, probably you’ll have to perform a huge refactor.

Also, it’s difficult not to agree that a software has a good design when it’s not rigid, not fragile and not immobile or coupled, and it behaviors as it should.

It’s totally fair if the reader is thinking why we’re talking about this, and what’s its relation with the DIP. The answer is quite simple, this principle is directly proportional to a good design. That means when we respect this principle, our developed software tends not to be rigid, fragile and immobile. We’ll show in the next section why this can make sense.

Bad Design Reasons

We’ve just defined each bad design characteristics. So now let’s point their causes.

A software can be rigid due to:

  • Disrespect the OCP principle, because when some extension in some parts of this software are required, there will be necessary to change another parts, since they are hypothetically not closed for modifications.
  • The presence of structures depending upon another structures, instead of depending on abstractions. This is because changes in those structures would affect the first one in terms of rebuilding or retesting, for example.

A software can be fragile due to:

  • Again, the OCP isn’t respected, since when some extension in some parts of this software are required, there will be necessary to change another parts, since they are hypothetically not closed for modifications. Sometimes, this cannot be noticed in build time. So, the necessary modifications wouldn’t be done, and these other parts would be broken.
  • Again, there are some structures depending upon another structures, instead of depending on abstractions. This happens because changes in those structures would affect the first one for example breaking its current functionality.

Finally, a software can be immobile and coupled because of:

  • One more time, structures depending upon another structures, instead of depending on abstractions. This would occur because with structures depending upon another ones, trying to reuse would be so much difficult and it would turn the code coupled.

Voilá! If we come back to the DIP definition, we may notice this statement that appeared repeatedly in each one of the bad design characteristics reasons is basically one DIP definition’s statement:

structures depending upon another structures, instead of depending on abstractions.

Therefore, that’s the reason why this principle is directly proportional to a good design, because not following it means you’ll be developing a rigid, fragile, immobile and coupled software, which is a bad designed one.

Relation with the others SOLID principles

As we’ve already said, the DIP is resulted from the OCP and LSP. That’s because developing driven by abstractions usually solves problems occurred from disrespecting OCP and LSP. If the reader come back to the OCP and LSP articles, all the four examples were solved through abstractions. We made classes depend upon abstractions (protocols) instead of other classes, and doing this, we also made details (classes) depend upon abstractions. So basically, we follow the DIP in order to also follow the OCP and LSP.

Now let’s dive in an example, to make things clearer. But, before go to the example, we strongly recommend to quickly look at the OCP and LSP articles, just to remember their examples and see how they were solved using abstractions. Maybe, they can be even better ones than we’re going to see here.

Example in Swift

Consider the following example, where we have a struct Product. This struct represents a single product and will be used by a ViewController to visually present a list of products.

However, before implementing the ViewController, let’s take a look this really simple implementation of a Network structure, which will be used in this example just to allow the View Controller to fetch the products from an API.

Please, ignore the fact this Network structure is poorly implemented. A Network definitely should be much more complex and well structured, able to handle not just one type of http method, it should accept and handle any URL, and also any type of response, it should handle errors, parse the API data response, instead of returning a mocked object, it should contain a certificate pinning implementation for security reasons, etc.

We could list lots of other important things about a well designed Network structure (or even Network framework, since this could be really complex and demand more than just one struct or class), but we won’t, because this is not the purpose here. It can be another article’s topic, and we could talk about this another day.

The main reason to choose a Network structure is that almost every project would have one structure like this, in order to perform requests to a server application. Also, a Network is a good example of a low-level module/structure.

Finally and not less important, we have a View Controller class, that represents a screen where the products will be fetched using this Network, the ExampleScreenViewController.

The method getProducts() in the ExampleScreenViewController is responsible to fetch the list of products from the API using the Network, as we’ve already mentioned.

Here some relevant observations: we’re not implementing the view and any other logic of this View Controller, we’re implementing this View Controller through View Code, and so not using the Interface Builder, and finally, we’re not presenting the products in the View Controller’s view, since it’s just an example and it wouldn’t aggregate anything to what we’d like to explain here.

Let’s then take a better look into this example to understand which problems we can observe here, and why it’s breaking the DIP.

Problems in breaking the DIP

Rigidity and Fragility

The first problem we can notice is our software is rigid and fragile. If we needed to change the struct Network for some reason (a refactoring, an extension, whatever), this change would affect the ExampleScreenViewController. This happens because the View Controller depends on the Network, since it has a reference for it. This is not really good, because this hypothetical change would force us unnecessarily to rebuild and retest the ExampleScreenViewController. This is a problem we’ve already seen, if you’ve read all this series’ other four articles. It appeared In each one of them, in SRP, OCP, LSP and ISP. And now, it appears here in DIP.

Besides, that hypothetical change could force changes in the ExampleScreenViewController, what could be avoided. These changes are unnecessary and would make this implementation more difficult and longer. Also, these changes (the desired one, and the undesired View Controller’s change) could break the ExampleScreenViewController code in build time. All of this could be avoided if the DIP would be respected.

Immobility and Coupling

The other problem we’ll see in this example is the immobility and coupling that it causes. If we’d like to reuse the ExampleScreenViewController in another context (another module or project), we’d be forced to bring together with it the Network structure. This can’t make a significant difference here, since the Network structure is really small, but if we think in a real project, a Network can be really complex, have many files and structures, and it can represent a whole module. So, it would make difference, and we definitely wouldn’t like to import an unnecessary whole module in another project.

Probably in this case would be better and easier to create the ExampleScreenViewController in the other context from scratch. And again, this would take time, which takes cost and resources, and it could be avoided if we had respected the DIP.

Respecting the DIP

Well, in order to respect the DIP, we just have to follow what the principle says: let’s make the high level and the low level structures depend on abstractions. Also, let’s make the details depend on abstractions.

We’ve therefore created some protocols (abstractions): ProductProtocol and NetworkProtocol.

Attention to the second protocol, where we’ve changed the dependency of NetworkProtocol.getProducts(for:completion:) method to be upon an abstraction, ProductProtocol. It now doesn’t depend on a detail anymore. We'’ also made the Product struct to conform to this new abstraction.

Now, coming back to Network structure, we’ve made it to conform to NetworkProtocol. Also, we’ve applied the small difference in the method getProducts(for:completion:), required by the protocol implementation, and that was already changed above.

Finally, the last changes we’ve had are the properties inside ExampleScreenViewController, where we’ve changed the concrete dependency of this View Controller upon the Network and the array of Products to abstractions. That means both references are for protocols now. Besides, we’ve changed the init of this View Controller to receive objects of type NetworkProtocol and [Product].

After all those changes, we’ve made that example to respect the DIP, since the high level implementation (ExampleScreenViewController) and the low level one (Network) now depend on abstractions. The other reason for this accomplishment is because the details (Network and ExampleScreenViewController) now depend on abstractions (the protocols) too.

Other Advantages about Abstractions

Besides not developing a bad designed code, when we develop softwares depending upon abstractions instead of details, we also have two considerable advantages:

  1. It’s easier to substitute an old and legacy implementation to a new one. For instance, in our example, we could change the Network implementation easily after solved the DIP break. Since we have a protocol dependency, we could just implement a new class named NewNetwork, with different low level implementations, but conforming to the same contract, established by the protocol.
  2. It’s easier to implement unit tests for structures that depends upon abstractions than structures that depend upon details. Creating mocks and spies are definitely options in that first case, what make unit testing much easier than have to use a concrete implementation dependency, what may also may be considered wrong, since it can bring us false-positives in those tests.

Overview DIP

  • This principle says
  1. High level modules should not depend upon low level modules. Both should depend upon abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.
  • This principle are resulted from rigorous use of those two principles (OCP and LSP)
  • A bad designed code is rigid, fragile, immobile and coupled.
  • If we don’t respect the DIP when developing a software, this software may become a bad designed software.
  • The advantages of respecting this principle are:
  1. Not bad designed softwares
  2. Facilities in replacing peaces of software (classes, structs, modules) when they conform to protocols
  3. It’s easier and better when writing unit tests for structures that depends upon abstractions

References

  1. What is SOLID
  2. Who is Uncle Bob
  3. Robert C. Martin. Design Principles and Design Patterns, 2000

This was the last article of the series of five articles about SOLID and its use in Swift. I hope you have enjoyed and feel free to leave feedbacks, suggest improvements or even send me some messages.

--

--

Rodrigo Maximo
Movile Tech

Lead Mobile Engineer at Nubank |  iOS Engineer