Introduction to SOLID Principles: The Heroic Saga of Code
Anh Tu Nguyen
Posted on October 13, 2024
Welcome, fellow adventurer, to the realm of software design, where dragons of spaghetti code threaten the kingdom of Clean Code! Fear not, for today we arm ourselves with the legendary SOLID principles. These principles are not just any boring rules; they’re your enchanted shield and sword in the battle for maintainable, scalable, and bug-free code.
But first, what is SOLID in the name of code refactoring? It stands for five knightly virtues—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
Grab your mouse and keyboard. Let’s embark on this journey together, full of twists, turns, and maybe even a few bad jokes.
S: Single Responsibility Principle (SRP) - The Anti-Monolithic Spell
Definition:
A class should have one, and only one, reason to change. Don’t give your class an existential crisis!
In Action:
Imagine a medieval blacksmith who makes swords, bakes bread, and does your taxes. He’s trying to sharpen your sword while thinking about tax deductions, and there’s flour in the forge. Messy, right? That’s what happens when your class tries to do too much!
Instead, keep your classes focused. Have one class for sword-making, another for baking, and a third for dealing with the tax trolls.
Example:
// Bad SRP example
type Blacksmith interface {
MakeSword()
BakeBread()
FileTaxes()
}
// Better SRP example
type Blacksmith interface {
MakeSword()
}
type Baker interface {
BakeBread()
}
type TaxFiler interface {
FileTaxes()
}
A knight with a single responsibility sleeps better at night. Trust me.
O: Open/Closed Principle (OCP) - The Door is Closed, but Feel Free to Paint it!
Definition:
Your classes should be open for extension but closed for modification. It’s like having a fancy door that you can decorate but never remove.
In Action:
You’ve built a loyal, trustworthy squire. But now you need him to do more—fetch water, sharpen your sword, and polish your armor. Don’t rewrite his DNA (code)! Just add to his training.
Instead of modifying the Squire class every time, create extensions. Keep the door closed, but hang a stylish sword on it!
Example:
// Without OCP
type Squire struct {
FetchWater()
SharpenSword()
}
// With OCP: Add without modifying
type Task interface {
Perform()
}
type FetchWaterTask struct {}
func (f FetchWaterTask) Perform() {
// Fetch water
}
type SharpenSwordTask struct {}
func (s SharpenSwordTask) Perform() {
// Sharpen sword
}
Now your code can evolve as majestically as a knight’s skill set without rewriting the core.
L: Liskov Substitution Principle (LSP) - The “Imposter Syndrome” Cure
Definition:
If a class inherits from another, it should be able to replace the parent class without causing mayhem (or, in our case, runtime errors).
In Action:
Imagine you own a stable. Horses gallop, and unicorns… well, they gallop too, but with glitter. Now, if you swapped your horse for a unicorn, you wouldn’t expect the unicorn to explode or refuse to gallop, right? That’s LSP in action.
Your subclasses (like the unicorn) should behave like the parent (the horse), without any unexpected behavior (no glitter explosions, please).
Example:
// LSP violation
type Horse struct {
Gallop()
}
type Unicorn struct {
Gallop() {
// Throws glitter instead of galloping!
}
}
Following LSP is like having a well-behaved unicorn. It looks magical but doesn’t break the barn.
I: Interface Segregation Principle (ISP) - The Curse of Unnecessary Contracts
Definition:
Clients should not be forced to depend on interfaces they don’t use. Consider it breaking down contracts so you only sign what’s necessary.
In Action:
Let’s say you’re a knight with a sword. Your knight’s guild gives you a contract that also requires you to know archery, jousting, and dragon taming. But you just want to swing your sword! Don’t make a sword-wielding knight learn dragon-taming unless they want to.
Example:
// Bad ISP Example
type Warrior interface {
FightWithSword()
RideDragon() // Not every warrior rides dragons!
}
// Better ISP Example
type Swordsman interface {
FightWithSword()
}
type DragonRider interface {
RideDragon()
}
This way, you can focus on what you do best—sword-swinging—and leave the dragon-taming to the professionals.
D: Dependency Inversion Principle (DIP) - No More Royal Puppeteers
Definition:
High-level modules should not depend on low-level modules. Both should depend on abstractions. Let the peasants (low-level modules) and kings (high-level modules) follow the same laws (interfaces).
In Action:
Think of a knight who needs a weapon. Instead of demanding a specific sword from the blacksmith, they ask for any weapon that adheres to the Weapon interface. This way, they can use a sword, a bow, or even a banana (if someone implements it). The knight isn’t tied to one weapon, just the idea of a weapon.
Example:
// Without DIP
type Knight struct {
sword Sword
}
// With DIP
type Weapon interface {
Use()
}
type Knight struct {
weapon Weapon
}
func (k *Knight) Fight() {
k.weapon.Use()
}
Now the knight is flexible and can fight with any weapon, even a legendary potato cannon (if coded properly).
Conclusion: The Hero’s Journey to SOLID Code
The journey to mastering SOLID principles is full of challenges, much like slaying dragons. But with Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion by your side, your code will be as smooth as a bard’s ballad.
You now wield the mighty sword of maintainable design. Go forth and refactor with confidence, brave coder!
Let me know if you’d like to see any specific areas covered more deeply!
Posted on October 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.