When you're designing a new component for your codebase, you will usually only think of the component itself, and the objects that it interacts with directly. If you're designing a component that authenticates a user, you will typically only consider objects directly related to the authentication flow. You'll take into account that there's probably a network call, and maybe a central current user storage object. You don't want to spend time thinking about objects that are related to that network object since that's not something the component you're designing should care about. For example, if the network object caches responses from the server, or whether it uses some sort of configuration object. It's not relevant to the needs of the authentication flow. However, it's not uncommon to see code like the following in production:
struct Authenticator {
let networkingObject: Networking
func authenticate(using email: String, password: String, @escaping (Result<User, Error>) -> Void) {
if networkingObject.configuration.requiresAuthentication {
// authenticate
}
// proceed because we don't need to be authenticate
}
}
Code like the preceding snippet is called tightly coupled; it relies heavily on another object's implementation. In today's article, I will explain why tightly coupled code is a problem for flexibility, readability, and maintainability. You will also learn about an interesting principle called the law of Demeter. Let's dive right in, shall we?
Understanding why tight coupling is problematic
In the introduction of this article I showed you a code snippet that contained the following line of code:
networkingObject.configuration.requiresAuthentication
At first glance, you'll probably think there is nothing wrong with this code. The Authenticator
has access to a Networking
object. And we know that the networking object has a configuration, so we can access that configuration, and we can check whether the network's configuration says that authentication is required. Based on that line of code, we can draw a couple of conclusions:
- The
Authenticator
depends on a configuration value to determine whether authentication is required. - The network's configuration can not be private because that would prevent
Authenticator
from reading it. - The network's configuration can not be removed or changed significantly because that would break the
Authenticator
.
The three conclusions that I listed are all implicit. If you're a developer that's tasked with making changes to the Networking
object, and you've never worked on the Authenticator
, it's unlikely that you're aware of the Authenticator
and its needs. And even if you would take a quick look at the Authenticator
, it's not likely that you'll realize that changing the Networking
configuration will break your Authenticator
. Even the smallest changes to the Networking
object can now impact Authenticator
which is undesirable.
We call this kind of situation "tight coupling". The Networking
object depends heavily on the Authenticator
's implementation details. You might even say that we have an implicit dependency on a configuration in Authenticator
because, to function properly, the requiresAuthentication
property of a configuration object is used. Both implicit dependencies and tight coupling are problematic because it makes refactoring and changing code really complicated. Changing details in one object might cause other objects to break in unexpected ways. This can be frustrating and it often results in a significant increase in development time for new features, bug fixes and it makes solving tech debt incredibly hard.
Not only is code like this harder to maintain, but it's also in many ways very complicated to design code like this. To write code that is tightly coupled to other code, you need to know everything about all code in your codebase. You can only stitch together a chain of parameters like networkingObject.configuration.requiresAuthentication
when you have deep knowledge of the system and all components it. However, in my experience, this kind of code is not written because the author made a conscious decision to do so. This kind of code is often written when there hasn't been a lot of thought about the design of the code before writing it and the path to achieving the desired outcome is paved while you're walking it. What I mean by this is that a developer who writes code that's tightly coupled often tackles problems as they present themselves rather than trying to anticipate the way code will work upfront.
There are many ways to avoid writing tightly coupled code that has implicit dependencies. Let's take a look at one of these approaches now.
Decoupling code using the law of Demeter
The law of Demeter, also known as the principle of least knowledge presents a set of rules that should be followed in any codebase. The short explanation of this rule is that objects are only allowed to access properties, functions, and parameters that are directly available to them. If you keep that in mind, do you see how the following code violates this rule?
self.networkingObject.configuration.requiresAuthentication
I added self
here to make it clear how deep we have to go to read the requiresAuthentication
property. It's okay for self
to access networkingObject
since it's a property that's owned by self
, so that's all good. Since the networkingObject
exposes certain properties and methods that self
should interact with, it's also okay to access configuration
as long as we need to. The biggest problem is with accessing requiresAuthentication
. By accessing requiresAuthentication
, self
knows too much about the structure of the code. One way to sum up the rules of the law of Demeter that I like is provided on its wikipedia page:
- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
- Each unit should only talk to its friends; don't talk to strangers.
- Only talk to your immediate friends.
This list makes the idea very clear. Everything you access should be as close to the place you're accessing it from as possible.
So what if we still want to read requiresAuthentication
by going through the Networking
object? You might argue that the Networking
object should be the source of truth for whether authentication is required, which is fine. The important part is that we depend on as little implementation details as possible. So if the Networking
object's configuration
property holds the requiresAuthentication
property, we could refactor the code in Networking
to the following:
struct Networking {
private let configuration: Configuration
var requiresAuthentication: Bool { configuration.requiresAuthentication }
// The rest of the implementation
}
By refactoring the code, as shown above, the Authenticator
can now use networking.requiresAuthentication
rather than networkingObject.configuration.requiresAuthentication
, and that's much better. We're dependant on the configuration anymore, so we're free to make that private, and change it as needed. The source of the requiresAuthentication
property is now hidden, so we no longer violate the law of Demeter.
The big win here is that by only accessing properties that are "one level deep", we can now refactor our code more freely, and we only need to know about objects that we need to interact with directly. All dependencies are clear and there are no surprises hidden in our code anymore.
The downside is that this approach might require you to wrap a lot of code in very short methods or computed properties as we did in the refactored code snippet I showed you earlier. When you stick to the law of Demeter, it's not uncommon to have methods that look like the following example:
struct UserManager {
// Other code
func getAuthenticationStatus(completion: @escaping (Result<AuthenticationStatus, Error>) -> Void) {
authenticator.getAuthenticationStatus(completion)
}
}
It might seem tedious to write code like this at first. After all, it's much simpler to just write manager.authenticator.getAuthenticationStatus { _ in }
than to create a new method that only calls a different method. While this can be tedious at first, you'll find that once you need to make changes to how the Authenticator
determines whether somebody is authenticated, it's very convenient to be able to limit the work you're doing to a single place.
In summary
Today's article was something different than you might be used to from me. Instead of learning about something cool that exists on iOS or in Swift, you learned how louse coupling works in practice. I explained that loose coupling allows you to refactor with confidence and that it allows you to write code that is as independent of other objects as possible. You learned and saw that tight coupling can lead to incredibly complex code that is very hard to refactored.
I then explained to you that there is a rule about how much objects should know about each other and that this rule is called the law of Demeter, or principle of least knowledge. You saw that applying this law in your code leads to easy public interfaces and that it enforces loose coupling, which means that your code is much more flexible and simpler to reason about than code that doesn't adhere to the law of Demeter.
The law of Demeter is one of the few principles that I try to apply in every project, regardless of size, complexity or purpose. If you're not sure how to apply it in your project, have questions or if you have feedback, I love to hear from you on Twitter.
Top comments (0)