Software can never be fully decoupled!
Decoupled with intentional coupling is the goal.
Introduction
We often talk about how good software is decoupled.
In this post, I want to dive into coupling, the dangers of coupled software and why software can never be 100% decoupled.
What is coupling?
Things being coupled means they are connected in a pair or pairs.
In software engineering, parts of the system being coupled means they depend on each other.
So if part X changes, it will affect how part Y behaves.
The dangers of strong coupling
In software engineering, when parts of the system are coupled, they affect each other.
The stronger the coupling, the harder to maintain and change the software.
If we change something in one place, we may have to change it in another part of the system. It's a dangerous cycle. Because of one change we might have to change 4 other places due to strong coupling. Complexity that could've been avoided.
This is why we want to aim for low coupling.
Why never fully decoupled?
We aim for low coupling, not NO coupling.
No coupling doesn't make sense. Then our software wouldn't work. The pieces of the system will be connected. They form the entire software. Coupling will always exist.
Low coupling is the goal!
It's important to note separation of concerns and coupling are related but different things. We can separate e.g. a piece that processes orders and a piece that stores the orders. They might be tightly coupled despite being separated.
That's fine.
How to minimize coupling
Example
Let's take a look at a classic JavaScript example where we manage the shopping cart:
Bad example
// Global state for cart (demonstration purposes)
let cart = [];
// Function to add item to the cart
function addItemToCart(item) {
cart.push(item);
updateCartDisplay();
saveCartToLocalStorage();
}
// Function to update the cart display
function updateCartDisplay() {
const cartElement = document.getElementById('cart');
cartElement.innerHTML = ''; // Clear cart element
cart.forEach(item => {
const itemElement = document.createElement('div');
itemElement.innerText = `${item.name}: $${item.price}`;
cartElement.appendChild(itemElement);
});
}
// Function to save the cart to localStorage
function saveCartToLocalStorage() {
localStorage.setItem('cart', JSON.stringify(cart));
}
The problems with bad example
UI changes: If we decide to change the way the cart is displayed, we would need to rewrite the
updateCartDisplay
function. SinceaddItemToCart
directly callsupdateCartDisplay
, it would also need to be updated.Storage changes: If we move from using
localStorage
to a different method of storage, like a backend API, thesaveCartToLocalStorage
function will need to change, which meansaddItemToCart
will also need updating.
addItemToCart
is doing too much.
We can use dependency inject (aka inversion of control) to loose the coupling.
Good example
// Global state for cart (demonstration purposes)
let cart = [];
// Function to add item to cart (manipulates data only)
function addItemToCart(item, callback) {
cart.push(item);
if (callback) {
callback(cart);
}
}
// Independent function to update the cart display
function updateCartDisplay(cart) {
const cartElement = document.getElementById('cart');
cartElement.innerHTML = ''; // Clear cart element
cart.forEach(item => {
const itemElement = document.createElement('div');
itemElement.innerText = `${item.name}: $${item.price}`;
cartElement.appendChild(itemElement);
});
}
// Independent function to save the cart to localStorage
function saveCartToLocalStorage(cart) {
localStorage.setItem('cart', JSON.stringify(cart));
}
// Usage
addItemToCart({ name: 'Apple', price: 0.99 }, cart => {
updateCartDisplay(cart);
saveCartToLocalStorage(cart);
});
Points to keep in mind
I could go on for days talking about how to reduce coupling.
It's extremely important to be pragmatic.
Don't be dogmatic and take everything I mention below religiously:
Single Responsibility principle
- May require more code. More code is fine!
Interface Segregation Principle
Dependency Inversion Principle
DRY
YAGNI
Avoid global state
Favor composition over inheritance
Dependency injection
Law of Demeter
Design patterns
- Not all of them will help you in every situation. Be pragmatic. Use the ones that will help you.
If things are unrelated, pull them apart.
Microservices
Conclusion
Let's build loosely coupled software.
I had a lot of fun writing this haha :D