Clean Code Basics: SOLID
Let's defeat the SOLID 5 Bosses!
Hello again, clean coder đšâđ». In todayâs article, weâll play an RPG Game! Yaay! Your quest is the following: Defeat Five Bosses, called the Five SOLID Bosses! If you do defeat them, and Iâm sure you will (because I trust my readers and I know that they are great developers), you will become the new Boss in the developersâ community đ . Each boss represents a principle from the SOLID code design philosophy.
So, be ready and letâs dive in, ala barakati lah!
Boss 1: Single Responsibility Principle (SRP)
PS: I already wrote a detailled article about SRP principle but I will do a recap here. If you didnât read yet the article, I recommend you to do that.
The Rule: A class (or function) should do one thing and one thing only!
âThe Problem
Imagine youâre tasked to create a character profile for a game. You write this tkharbiqa haha:
class Character {
constructor(name, level) {
this.name = name;
this.level = level;
}
saveToDatabase() {
console.log(`Saving ${this.name} to the database...`);
}
calculateExperience() {
return this.level * 100;
}
}Why Itâs Bad: This class is doing too much! Itâs a character, a database handler, and a calculator! SRP hates this đ€Ź!
â
The Solution
Split the responsibilities into different classes or functions:
class Character {
constructor(name, level) {
this.name = name;
this.level = level;
}
}
class DatabaseHandler {
static save(character) {
console.log(`Saving ${character.name} to the database...`);
}
}
function calculateExperience(level) {
return level * 100;
}Now, you have divided responsibilities, and you can also re-use them whenever you need too.
đSRP BOSS Defeated đȘđ
Boss 2: Open/Closed Principle (OCP)
The Rule: Code should be open for extension but closed for modification.
âThe Problem
You have a magic weapon that only deals fire damage. But now you need ice, lightning, and poison damage! You might be tempted to modify the original code like this:
// main.js
class FireWeapon {
attack() {
console.log('Dealing fire damage!');
}
}// main.js
// Let's modify the code
class IceWeapon {
attack() {
console.log('Dealing ice damage!');
}
}Why Itâs Bad: Modifying existing classes is riskyâwhat if something breaks? (Iâm sure it will break haha)
â
The Solution
Use inheritance or composition to extend functionality without modifying the original code:
class Weapon {
attack() {
throw new Error('Attack method must be implemented!');
}
}
class FireWeapon extends Weapon {
attack() {
console.log('Dealing fire damage!');
}
}
class IceWeapon extends Weapon {
attack() {
console.log('Dealing ice damage!');
}
}
const fireSword = new FireWeapon();
fireSword.attack(); // Dealing fire damage!The base class remains untouched, then you can extend and add your custom implementations.
đOCP BOSS Defeated đȘđ
Boss 3: Liskov Substitution Principle (LSP)
The Rule: Subtypes should be substitutable for their base types without breaking the program. I know this sentence looks terrible haha, but letâs look at the example I prepared for you:
âThe Problem
Letâs say your has a Enemy Class, but after a while a new feature comes into place, a flying enemy. You decide to add it to your existing Enemy class like that:
class Enemy {
move() {
console.log('Enemy moves forward.');
}
}
class FlyingEnemy extends Enemy {
move() {
throw new Error('Flying enemies donât walk!');
}
}Why Itâs Bad: A FlyingEnemy isnât behaving like a proper Enemy! So calling move() crashes the game by throwing an Error.
â
The Solution
Use interfaces (or careful class hierarchy) to enforce proper behavior:
class Enemy {
move() {
console.log('Enemy moves forward.');
}
}
class FlyingEnemy {
fly() {
console.log('Flying enemy soars through the skies.');
}
}
const goblin = new Enemy();
const dragon = new FlyingEnemy();
goblin.move(); // Enemy moves forward.
dragon.fly(); // Flying enemy soars through the skies.No more broken behaviors, each enemy does what itâs supposed to now.
đLSP BOSS Defeated đȘđ
Boss 4: Interface Segregation Principle (ISP)
The Rule: A class should not be forced to implement methods it doesnât use! (I see this a lot in code reviews, please be careful about it).
âThe Problem
You create a generic PlayableCharacter interface:
class PlayableCharacter {
move() {}
attack() {}
heal() {}
}But what if you have a healer who canât attack?
class Healer extends PlayableCharacter {
attack() {
throw new Error('Healer cannot attack!');
}
}Why Itâs Bad: Forcing classes to implement irrelevant methods leads to confusion and bugs!
â
The Solution
Split your interfaces into smaller, specific ones:
class Movable {
move() {
console.log('Moving...');
}
}
class Attacker {
attack() {
console.log('Attacking!');
}
}
class Healer {
heal() {
console.log('Healing...');
}
}
const cleric = new Healer();
cleric.heal(); // Healing...Everyone plays to their strengths without awkward side gigs. âïž
đISP BOSS Defeated đȘđ
Boss 5: Dependency Inversion Principle (DIP)
The Rule: High-level modules should not depend on low-level modules. Both should depend on abstractions. Again, it looks like a difficult sentence to understand right away haha, but letâs explore the example:
âThe Problem
Your game engine depends on a specific Database class:
class Database {
save(data) {
console.log('Saving to the database...');
}
}
class GameEngine {
constructor() {
this.db = new Database();
}
saveGame(data) {
this.db.save(data);
}
}Why Itâs Bad: If you switch to another storage method (like a cloud API), the GameEngine class needs to change.
â
The Solution
Introduce an abstraction (like an interface or a base class):
class Storage {
save(data) {
throw new Error('Method not implemented!');
}
}
class Database extends Storage {
save(data) {
console.log('Saving to the database...');
}
}
class CloudStorage extends Storage {
save(data) {
console.log('Saving to the cloud...');
}
}
class GameEngine {
constructor(storage) {
this.storage = storage;
}
saveGame(data) {
this.storage.save(data);
}
}
const cloudStorage = new CloudStorage();
const engine = new GameEngine(cloudStorage);
engine.saveGame({ level: 10 }); // Saving to the cloud...Now, your GameEngine works with any storage method, like a true abstraction wizardđ§.
đDIP BOSS Defeated đȘđ
Congrats! Youâve beaten the 5 SOLID Bosses!
Iâm really proud of you Boss! Youâve defeated all five SOLID bosses. Your code is now cleaner, more modular, and ready for any challenge. Go forth and write code that other developers will actually enjoy reading!
PS: I also recommend you to read my series about clean code principles, if you didnât.
Thatâs it for todayâs tutorial, I hope you enjoyed it! Ciaos!
đ Don't forget to subscribe to my blog and YouTube channel to stay updated with all the latest content!


