DEV Community

Cover image for SOLID Code, Solid Results
Kolawole Yusuf
Kolawole Yusuf

Posted on

SOLID Code, Solid Results

Looking at a codebase had me questioning how I got tangled up in such complexity. Each line of code felt like a maze of dense forest without a map.

When I first heard about SOLID principles, I'll be honest, I rolled my eyes. Another set of academic-sounding programming rules that seemed more like theoretical mumbo-jumbo than practical advice. I mean, who has time to think about complex design principles when you're just trying to get code working, right?

But something changed as I dove deeper into more complex software projects. Those seemingly abstract guidelines started to make real sense. I began to see how these principles weren't just academic theories, but powerful tools that could actually make my life as a developer easier. Projects that used to feel like tangled messes started to become more manageable, more logical.

Now, as an experienced developer, I code with SOLID principles in mind, which has consistently helped me write maintainable, scalable, and robust code. SOLID represents five principles which are like a compass that helps developers to navigate through the complex system and to develop such software which are not only reliable but also beautiful, flexible and long lived. Robert C Martin, also known as Uncle Bob developed the principles, which are widely regarded as the cornerstone of effective software design.

Let me break down each principle with real world examples from my own development experience:

S - Single Responsibility Principle (SRP)
Think of SRP like organizing your toolbox. Each tool has a specific purpose, and so should your classes.
Here's a before and after sample code:

// Before: A messy, do-everything class
class UserManager {
  createUser(userData) {
    // Validate user data
    if (!userData.email || !userData.password) {
      throw new Error('Invalid user data');
    }

    // Save user to database
    const db = new Database();
    db.save('users', userData);

    // Send welcome email
    const emailService = new EmailService();
    emailService.sendWelcomeEmail(userData.email);
  }
}

// After: Separated responsibilities
class UserValidator {
  validate(userData) {
    if (!userData.email || !userData.password) {
      throw new Error('Invalid user data');
    }
  }
}

class UserRepository {
  save(userData) {
    const db = new Database();
    db.save('users', userData);
  }
}

class UserNotificationService {
  sendWelcome(email) {
    const emailService = new EmailService();
    emailService.sendWelcomeEmail(email);
  }
}

class UserManager {
  constructor(validator, repository, notificationService) {
    this.validator = validator;
    this.repository = repository;
    this.notificationService = notificationService;
  }

  createUser(userData) {
    this.validator.validate(userData);
    this.repository.save(userData);
    this.notificationService.sendWelcome(userData.email);
  }
}
Enter fullscreen mode Exit fullscreen mode

O - Open/Closed Principle (OCP)
I learned this principle the hard way. Imagine building a payment processing system that needs to support multiple payment methods without rewriting existing code.

// Payment processor that's open for extension
class PaymentProcessor {
  processPayment(paymentMethod, amount) {
    switch(paymentMethod) {
      case 'stripe':
        return this.processStripe(amount);
      case 'paypal':
         return this.processPayStack(amount);
      default:
        throw new Error('Unsupported payment method');
    }
  }

  processStripe(amount) {
    // Stripe processing logic
  }

  processPayPal(amount) {
    // PayPal processing logic
  }


}

// Better approach using strategy pattern
class PaymentStrategy {
  process(amount) {
    throw new Error('Must implement process method');
  }
}

class StripePayment extends PaymentStrategy {
  process(amount) {
    // Specific Stripe processing
  }
}

class PayPalPayment extends PaymentStrategy {
  process(amount) {
    // Specific PayPal processing
  }
}

class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

  processPayment(amount) {
    return this.strategy.process(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

L - Liskov Substitution Principle (LSP)
This principle taught me to ensure that derived classes can replace base classes without breaking the application.

// Problematic implementation
class SportsCar {
  accelerate() {
    console.log('Rapid acceleration, 0-60 in 3.2 seconds');
  }

  turboBoost() {
    console.log('Engaging turbo boost for maximum speed');
  }
}

class ElectricCar extends SportsCar {
  turboBoost() {
    // Electric cars don't have traditional turbo boost
    throw new Error('Turbo boost not supported in electric vehicles');
  }
}

class LuxuryCar extends SportsCar {
  turboBoost() {
    // Luxury car prioritizes comfort over speed
    console.log('Gentle acceleration mode engaged');
  }
}

// Better approach following LSP
class Car {
  accelerate() {
    throw new Error('Must implement accelerate method');
  }
}

class PerformanceCar extends Car {
  accelerate() {
    console.log('High-performance acceleration');
  }

  turboBoost() {
    console.log('Maximum speed boost activated');
  }
}

class ElectricPerformanceCar extends Car {
  accelerate() {
    console.log('Instant electric acceleration');
  }

  // No turbo boost, but maintains the car's core functionality
}

class LuxuryCar extends Car {
  accelerate() {
    console.log('Smooth, controlled acceleration');
  }
}
Enter fullscreen mode Exit fullscreen mode

I - Interface Segregation Principle (ISP)
I used to create massive interfaces that forced classes to implement methods they didn't need.

// Before: Bloated interface
class AllInOnePrinter {
  print() { /* print logic */ }
  scan() { /* scan logic */ }
  fax() { /* fax logic */ }
}

// After: Segregated interfaces
class Printer {
  print() { /* print logic */ }
}

class Scanner {
  scan() { /* scan logic */ }
}

class FaxMachine {
  fax() { /* fax logic */ }
}

class MultiFunctionDevice {
  constructor(printer, scanner, faxMachine) {
    this.printer = printer;
    this.scanner = scanner;
    this.faxMachine = faxMachine;
  }
}
Enter fullscreen mode Exit fullscreen mode

D - Dependency Inversion Principle (DIP)
This principle changed how I think about module dependencies.

// Dependency Injection
class NotificationService {
  constructor(communicationStrategy) {
    this.communicationStrategy = communicationStrategy;
  }

  notify(message) {
    this.communicationStrategy.send(message);
  }
}

class EmailStrategy {
  send(message) {
    // Email sending logic
  }
}

class SMSStrategy {
  send(message) {
    // SMS sending logic
  }
}

// Usage
const emailNotification = new NotificationService(new EmailStrategy());
const smsNotification = new NotificationService(new SMSStrategy());
Enter fullscreen mode Exit fullscreen mode

Why I can't do without SOLID
Implementing these principles has transformed how I build software:

  1. My code is more readable
  2. Changes are less painful
  3. Testing becomes significantly easier
  4. I can add features without a complete rewrite

My Advice for Getting Started

  1. Review your code regularly
  2. Practice these principles in small, manageable chunks

Conclusion
SOLID principles are the closest thing to magic in software development. They have transformed complex, brittle code into flexible and maintainable systems for me. Great software is thoughtfully crafted and continually improved

Top comments (0)