WHAT IS SOLID?
If you are a dev and have browsed this network, you've probably seen dozens of posts about this topic...
So, straight to the point, SOLID is a set of 5 principles that help improve the quality, scalability, and maintainability of software projects. And here on this network, I will share my experience with it!
SINGLE RESPONSIBILITY PRINCIPLE
A class should have one, and exclusively one, reason to change.
In other words, we shouldn't have a single class doing everything: receiving data, validating requests, applying business rules, and accessing the database...
This class that “does it all” is the so-called “God Class”, and it is an anti-pattern.
Code Example:
// ❌ Anti-pattern: God Class doing too much
class UserAccount {
createAccount(user: any) {
// 1. Validates request
if (!user.email) throw new Error("Email is required");
// 2. Applies business rules
user.status = "active";
// 3. Accesses the database
database.save(user);
// 4. Sends email
emailService.send("Welcome!");
}
}
// ✅ SOLID: Separated responsibilities
class UserValidator {
validate(user: any) { /* validation logic */ }
}
class UserRepository {
save(user: any) { /* DB logic */ }
}
class EmailSender {
send(message: string) { /* email logic */ }
}
OPEN - CLOSED PRINCIPLE
The integral parts of a code should be open for extension and closed for modification.
The idea is to avoid relying on conditional checks using if-else and switch. When a new requirement arrives, you add the behavior by creating something new, without modifying the old code.
Code Example:
// ❌ Anti-pattern: Modifying existing code for every new type
class PaymentProcessor {
process(paymentType: string, amount: number) {
if (paymentType === "credit") {
// process credit card
} else if (paymentType === "paypal") {
// process paypal
} // Adding a new type requires modifying this class
}
}
// ✅ SOLID: Open for extension via interfaces
interface PaymentMethod {
process(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
process(amount: number) { /* process credit card */ }
}
class PayPalPayment implements PaymentMethod {
process(amount: number) { /* process paypal */ }
}
// Now you can add new payment methods without touching existing code!
LISKOV SUBSTITUTION PRINCIPLE
A parent class must be able to be substituted by its child classes without breaking the application.
In practice, this helps to organize the idea of inheritance, as it prevents us from extending a parent class only to later remove an already implemented method or do a throw new Error('Not implemented'). Making us much more careful during planning.
Code Example:
// ❌ Anti-pattern: Breaking substitution
class Bird {
fly() { console.log("Flying..."); }
}
class Penguin extends Bird {
fly() {
throw new Error('Not implemented: Penguins cannot fly');
}
}
// ✅ SOLID: Better planning with abstractions
class Bird {
eat() { console.log("Eating..."); }
}
class FlyingBird extends Bird {
fly() { console.log("Flying..."); }
}
class Penguin extends Bird {
swim() { console.log("Swimming..."); }
}
INTERFACE SEGREGATION PRINCIPLE
No class should be dependent on methods it doesn't need.
An example would be a "Machine" interface that forces the methods print(), scan(), and fax(). Not every machine has these 3 methods.
So we create the interfaces IPrinter, IScanner, and IFax. Now a machine signs only the contracts it actually needs and fulfills.
Code Example:
// ❌ Anti-pattern: Forcing unnecessary methods
interface IMachine {
print(): void;
scan(): void;
fax(): void;
}
class SimplePrinter implements IMachine {
print() { console.log("Printing..."); }
scan() { throw new Error("Not supported"); } // Doesn't need this
fax() { throw new Error("Not supported"); } // Doesn't need this
}
// ✅ SOLID: Segregated interfaces
interface IPrinter { print(): void; }
interface IScanner { scan(): void; }
interface IFax { fax(): void; }
class SimplePrinter implements IPrinter {
print() { console.log("Printing..."); }
}
class MultiFunctionPrinter implements IPrinter, IScanner, IFax {
print() { console.log("Printing..."); }
scan() { console.log("Scanning..."); }
fax() { console.log("Faxing..."); }
}
DEPENDENCY INVERSION PRINCIPLE
High-level modules should not depend on low-level details. Both should depend on abstractions (interfaces).
In other words, instead of your class directly instantiating a connection to the database or an external service, it should receive it from the outside. It is this inversion that allows creating mocks for tests or swapping tools in the backend without breaking your business rules.
Code Example:
// ❌ Anti-pattern: High-level module depends on low-level detail
class MySQLDatabase {
save(data: any) { /* saves to MySQL */ }
}
class UserService {
private db = new MySQLDatabase(); // Tight coupling
createUser(user: any) {
this.db.save(user);
}
}
// ✅ SOLID: Depending on abstractions injected from the outside
interface IDatabase {
save(data: any): void;
}
class MySQLDatabase implements IDatabase {
save(data: any) { /* saves to MySQL */ }
}
class MongoDatabase implements IDatabase {
save(data: any) { /* saves to Mongo */ }
}
class UserService {
// Receives the dependency from outside (Dependency Injection)
constructor(private db: IDatabase) {}
createUser(user: any) {
this.db.save(user);
}
}
GOING DEEPER
SOLID is a set of complex principles, and you can't summarize everything in a simple bunch of sentences.
It is necessary to research, read, practice, interpret... So I intend to bring a series of new content here, unraveling them in detail. At least 1 topic per principle, with details, examples, and code.
My Links
Github: victor-lis-bronzo
Linkedin: victor-lis-bronzo
Portfolio: portfolio.victorlisbronzo.me
Coolest Portfolio: victorlisbronzo.me
Leave your reaction ❤️
Which of these 5 principles do you find the most difficult to apply in your daily life?
Top comments (0)