The article discusses the importance and methods of writing decoupled code in Swift, emphasizing its significance for testable and maintainable software.
Abstract
In the article titled "How to Write Decoupled Code in Swift," the author, Ronen Harati, builds upon the concept of unit testing's importance by delving into the practical aspects of writing decoupled code. The article outlines the drawbacks of tightly coupled code, where dependencies are hardcoded and cannot be changed, and explains how decoupling can lead to more testable and flexible code. It introduces different levels of coupling severity, from tightly coupled to fully decoupled, and illustrates a "decoupling ladder" with examples in Swift. The author guides readers through refactoring a tightly coupled class, NaughtyClass, into a more decoupled state by using dependency injection, protocols, and closures. The article concludes with the author's reflection on the journey of decoupling and invites readers to consider the coupling levels in provided examples, setting the stage for a follow-up article on dependency injection containers.
Opinions
The author believes that decoupled code is essential for writing unit tests, as it allows for mocking dependencies and isolating the class under test.
The article suggests that tightly coupled code is not only difficult to test but also hinders future modifications and scalability of the software.
The author advocates for dependency injection as a method to reduce coupling, allowing for greater flexibility and easier testing.
The author introduces a novel concept of a "decoupling ladder" to help developers understand and improve the level of decoupling in their code.
The author expresses that while achieving full decoupling is possible and sometimes beneficial, it may not always be the most practical approach for every scenario.
The article encourages interactive learning by prompting the reader to think about coupling issues and solutions at various points in the text.
In this article, I will discuss how to write decoupled code more practically. The examples will be in Swift but they apply to all other languages. This article is a continuation of my previous article, Why Write Unit Tests, whereI discussed the importance of writing unit tests.
Let’s start off with what decoupled code is, and why it is important in general and more so when writing unit tests. To understand what decoupled code is, let’s talk about coupled code first.
Coupled code is where a class’ dependencies can’t be changed, meaning the dependencies are hardcoded and use a specific implementation. Let’s take a look at an example
NaughtyClass creates an instance of sessionManager, and when btnLogoutTapped is called, logout is called on the sessionManager object.
This may seem fine at first and all functionalities will work, however, this code is tightly coupled. Try to think about why NaughtyClass is in fact naughty, I’ll wait…
SessionManager is a dependency that can not be replaced, meaning that the logout implementation can’t be changed at runtime. This isn’t a good design in general because what if, in the future, we have a few different kinds of logouts, for example Facebook logout, Google logout, etc.
We won’t be able to easily add those cases, we will need to change the main logout method which will decide which logout to use. This change will affect NaughtyClass directly and may introduce a bug.
This is also bad for unit tests because we won’t be able to mock SessionManager. Mocking is a way to create a fake object that can replace SessionManager to have full control over all of NaughtyClass dependencies.
This is crucial for unit tests because we need to isolate the class being tested from all its dependencies.
In our example, it is not so hard to realize that our code is tightly coupled, however, in larger projects it’s not always so easy. When trying to figure out if a class is coupled or not, we can look at how easy it is to reuse it in a different project.
If you need to import many files just to use one class, that’s a pretty good indication that the class is tightly coupled. If you can re-use a class without importing too many or any files, that’s a good indication of how much the class is decoupled.
Levels of Severity
Let’s list four different levels of severity in coupled and decoupled code:
Tightly coupledis the strongest type of coupled code. In this case, as in the example above, a class holds its dependencies and they cannot be changed. This forces the class to use a specific implementation, meaning the class uses a specific object that cannot be replaced.
Coupledis a bit better than tightly coupled, but is still considered coupled. In this case, a class’ dependencies can be replaced, but are of a specific class. For example, if the dependency is set as a var instance, it can be replaced with a different object of the same class. This enables us to be able to set subclasses and override implementations to a certain extent, which is better than being tightly coupled.
Loosely coupledis when a class doesn’t depend on a certain object or class, but rather on a protocol/interface. In this case, dependencies don’t have a specific implementation, but rather a protocol/interface that they conform to. This allows us to create multiple implementations and use different ones at runtime, including mocking objects and classes for unit tests.
Decoupledis when a class doesn’t depend on a certain object, class, or protocol/interface. This may sound like the class has no dependencies, but we will see later on that this doesn’t have to be the case. In this case, classes can be reused in other projects without needing any other files.
Just as a note, not everybody divides different levels of severity in coupling this way.
Tightly coupled and coupled can be combined into tightly coupled. Loosely coupled and decoupled can also be combined into either one. However, I think it makes more sense to divide them this way where we can work our way up the decoupling ladder (object > class > protocol > closure).
The Decoupling Ladder
So, now let’s take a look at our previous example and work our way up the decoupling ladder. As we mentioned, NaughtyClassis tightly coupled, try to take a moment to think how we can make it just coupled, I’ll wait…
OK, so, to make it coupled, we need the dependency to use a specific class rather than a specific object. The easiest way to do this is to just change the sessionManagerinstance to be a var instance instead of alet instance.
Now that sessionManager is a var it can be replaced with a different object of the same class, or a subclass of SessionManager at runtime.
This is OK, but there is a better way to initialize the sessionManager instance. We can inject this dependency from the outside world, which is surprisingly called dependency injection.
Let’s take a look at what that would look like.
Dependency injection enables us to initialize our class with the correct object, rather than creating the object within the class and replacing it with another.
This is better because sessionManager can be a shared instance that is injected, where beforehand, a new object was created every time. Even though we inject our dependency, our code is still coupled.
The reason is that our class depends on a specific class, but it is an upgrade from depending on a specific object. The next step is writing our code loosely coupled, take a moment and think of how we can achieve that…
OK, so, for our class to be loosely coupled we need it to depend on a protocol/interface rather than an object or class. Let’s first create our protocol.
Now that we have declared our protocol, let’s use it instead of a class.
Now that we have used a protocol for our dependency, our class is loosely coupled! This enables us to use any object or class that conforms to the SessionManagerProtocol.
We can now create mock objects and inject them when we write our unit tests. Our class, however, still has a dependency on a protocol, how can we make it completely decoupled? Take a moment to think about that…
OK, so, we need to try to write our class in a manner that doesn’t depend on a specific object, class, or protocol, hmm. Let’s see what that might look like.
We can inject a closure that implements the logout logic. This enables anyone to implement the logout logic as long as the input and output of the closure match our declaration.
Our class is now fully decoupled, as it doesn’t depend on a specific object, class, or protocol.
I wouldn’t necessarily recommend writing our classes this way, and I’m sure there are other methods for writing a fully decoupled class, however, it’s good to know that it is possible to create a fully decoupled class.
I hope you have enjoyed our decoupling journey. Take a look at a few examples, I will leave it up to you to try and figure out what level of coupling they are. Feel free to post your answers in the comments.