Let’s talk SOLID
A comprehensive guide to the five principles that will transform your object-oriented programming and create more maintainable, scalable software.
Introduction
SOLID is an acronym for five object-oriented design principles introduced by Robert C. Martin (“Uncle Bob”) that help create maintainable, scalable, and robust software systems. These principles have become foundational in modern software development and are essential for writing clean, professional code.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have only one responsibility or job.
Real-world Analogy: Think of a specialized chef who only handles baking, versus a chef who bakes, manages inventory, serves customers, and cleans the kitchen. The specialized chef can focus on perfecting their craft.
Code Example
// Bad: Class has multiple responsibilities
class User {
private username: string;
private passwordHash: string;
constructor(username: string, passwordHash: string) {
this.username = username;
this.passwordHash = passwordHash;
}
validatePassword(password: string): boolean {
// Authentication logic
return true; // Simplified
}
saveToDatabase(): void {
// Database operations
console.log('Saving user to database');
}
generateReport(): string {
// Reporting logic
return 'User report';
}
}
// Good: Separate responsibilities
class User {
private username: string;
private passwordHash: string;
constructor(username: string, passwordHash: string) {
this.username = username;
this.passwordHash = passwordHash;
}
getUsername(): string {
return this.username;
}
}
class PasswordService {
validatePassword(user: User, password: string): boolean {
// Password validation logic
return true; // Simplified
}
}
class UserRepository {
save(user: User): void {
// Database operations
console.log('Saving user to database');
}
}
class UserReportGenerator {
generateReport(user: User): string {
// Report generation
return `Report for ${user.getUsername()}`;
}
}
Benefits: - Easier to maintain and test - Reduced coupling between components - More focused and readable code
2. Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Real-world Analogy: A power strip that allows you to plug in new devices without rewiring the entire electrical system.
Code Example
// Bad: Need to modify class for new shapes
interface RectangleWrong {
width: number;
height: number;
}
class AreaCalculatorWrong {
calculateArea(shape: RectangleWrong): number {
if (shape && 'width' in shape && 'height' in shape) {
return (shape as RectangleWrong).width * (shape as RectangleWrong).height;
}
return 0; // Need to modify for Circle, Triangle, etc.
}
}
// Good: Open for extension, closed for modification
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
class Triangle implements Shape {
constructor(private base: number, private height: number) {}
area(): number {
return 0.5 * this.base * this.height;
}
}
class AreaCalculator {
calculateArea(shape: Shape): number {
return shape.area(); // No modification needed for new shapes
}
}
// Usage
const shapes: Shape[] = [
new Rectangle(5, 4),
new Circle(3),
new Triangle(6, 8)
];
const calculator = new AreaCalculator();
const totalArea = shapes.reduce((sum, shape) => sum + calculator.calculateArea(shape), 0);
Benefits: - Reduces risk of introducing bugs - Promotes code reuse - Makes code more maintainable
3. Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
Real-world Analogy: Any TV remote should work with any TV without breaking functionality. You should be able to replace one remote with another without issues.
Code Example
// Bad: Violates LSP
class Bird {
fly(): void {
console.log("I can fly!");
}
}
class Penguin extends Bird {
fly(): void {
throw new Error("Penguins can't fly!"); // Violates LSP
}
}
// This breaks the program
function makeBirdFly(bird: Bird): void {
bird.fly(); // Throws error with Penguin!
}
// Good: Proper inheritance design
abstract class Bird {
abstract makeSound(): void;
}
class FlyingBird extends Bird {
fly(): void {
console.log("I can fly!");
}
}
class FlightlessBird extends Bird {
walk(): void {
console.log("I can walk!");
}
}
class Eagle extends FlyingBird {
makeSound(): void {
console.log("Screech!");
}
}
class Penguin extends FlightlessBird {
makeSound(): void {
console.log("Squawk!");
}
}
// This works correctly
function processBird(bird: Bird): void {
bird.makeSound(); // Works for all birds
if (bird instanceof FlyingBird) {
bird.fly(); // Only called on birds that can fly
}
}
Benefits: - Ensures inheritance hierarchies are well-designed - Prevents unexpected behavior - Makes polymorphism reliable
4. Interface Segregation Principle (ISP)
Definition: No client should be forced to depend on interfaces they don’t use.
Real-world Analogy: A specialized toolkit versus a massive Swiss Army knife. Sometimes you just need a screwdriver, not everything plus the kitchen sink.
Code Example
// Bad: Fat interface with unrelated methods
interface Worker {
work(): void;
eat(): void; // Not all workers need this
sleep(): void; // Not all workers need this
code(): void; // Not all workers need this
}
class Robot implements Worker {
work(): void {
console.log("Robot working");
}
eat(): void {
throw new Error("Robots don't eat!");
}
sleep(): void {
throw new Error("Robots don't sleep!");
}
code(): void {
console.log("Robot coding");
}
}
// Good: Segregated interfaces
interface Workable {
work(): void;
}
interface Coder {
code(): void;
}
interface Eater {
eat(): void;
}
interface Sleeper {
sleep(): void;
}
class Robot implements Workable, Coder {
work(): void {
console.log("Robot working");
}
code(): void {
console.log("Robot coding");
}
}
class Human implements Workable, Coder, Eater, Sleeper {
work(): void {
console.log("Human working");
}
code(): void {
console.log("Human coding");
}
eat(): void {
console.log("Human eating");
}
sleep(): void {
console.log("Human sleeping");
}
}
// Usage with proper interface separation
function makeSomeoneWork(worker: Workable): void {
worker.work();
}
function makeSomeoneCode(coder: Coder): void {
coder.code();
}
Benefits: - Prevents interface pollution - Makes systems more flexible - Reduces unnecessary dependencies
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Real-world Analogy: A power outlet (abstraction) that works with any device, not specific appliances. You can plug in a laptop, phone, or lamp without changing the outlet.
Code Example
// Bad: High-level depends on low-level concrete classes
class MySQLDatabase {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class FileLogger {
log(message: string): void {
console.log(`Logging to file: ${message}`);
}
}
class UserServiceBad {
private database = new MySQLDatabase(); // Tightly coupled
private logger = new FileLogger(); // Tightly coupled
createUser(userData: string): void {
this.database.save(userData);
this.logger.log("User created");
}
}
// Good: Dependency inversion with injection
interface Database {
save(data: string): void;
}
interface Logger {
log(message: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class PostgreSQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to PostgreSQL: ${data}`);
}
}
class FileLogger implements Logger {
log(message: string): void {
console.log(`Logging to file: ${message}`);
}
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`Logging to console: ${message}`);
}
}
class UserService {
constructor(
private database: Database, // Depends on abstractions
private logger: Logger // Depends on abstractions
) {}
createUser(userData: string): void {
this.database.save(userData);
this.logger.log("User created");
}
}
// Usage with dependency injection
const mysqlDb = new MySQLDatabase();
const consoleLogger = new ConsoleLogger();
const userService = new UserService(mysqlDb, consoleLogger);
// Easy to swap implementations
const postgresDb = new PostgreSQLDatabase();
const fileLogger = new FileLogger();
const userServiceWithPostgres = new UserService(postgresDb, fileLogger);
Benefits: - Easier testing with mock objects - Better modularity - Improved flexibility for changing implementations
Summary of Benefits
When you follow all five SOLID principles, you gain:
| Principle | Key Benefits |
|---|---|
| SRP | Focused classes, easier testing, reduced complexity |
| OCP | Extensible systems without breaking existing code |
| LSP | Reliable inheritance hierarchies, safe polymorphism |
| ISP | Minimal interfaces, reduced coupling |
| DIP | Loose coupling, easy testing, flexible architectures |
Overall System Benefits:
- Maintainability: Code is easier to understand, modify, and extend
- Testability: Components can be tested in isolation
- Flexibility: System can adapt to changing requirements
- Reusability: Components can be reused in different contexts
- Scalability: System can grow without becoming unwieldy
- Reduced Coupling: Components are less dependent on each other
Conclusion
The SOLID principles work together to create software that is robust, flexible, and easy to maintain over time. They’re not just academic concepts—they’re practical tools that solve real-world problems in software development.
Start by applying one principle at a time to your existing codebase. You’ll quickly see how these guidelines transform your code from “working” to “professional.” Remember, these principles are guidelines, not rigid rules—use them wisely to create better software.
Happy coding! 🚀
Tags: typescript, solid-principles, object-oriented-programming, software-design, clean-code