DEV Community

Cover image for SOLID principles in OOP for beginners
vivecodes
vivecodes

Posted on • Edited on

SOLID principles in OOP for beginners

SOLID principles are sets of best practices aimed to solve the common problems that developers face in object-oriented programming.

While design patterns provide concrete solutions to common design problems, the SOLID principles are way more abstract. They are intended to help in organizing and structuring the code, making it easier to manage and extend over time.

There are five SOLID principles in total:

Single Responsibility Principle (SRP)

Each entity (like a class, function, or method) should have only one responsibility or job.

Advantages:

  • Easier maintenance and updates
  • Clear purpose for each class
  • Simplified testing
  • Enhanced reusability
class User {
  userName: string;

  constructor(username: string) {
    this.userName = username;
  }

  getUserName() {
    return this.userName;
  }

  // this method violates SRP
  authenticate(password: string) {
    // implementation logic 
  }
}
Enter fullscreen mode Exit fullscreen mode

To follow SRP, a separate class should be created for authenticate user functionality.

class UserAuthentication {
  constructor(user: User) {}

  authenticate(password: string) {
    // implementation logic
  }
}
Enter fullscreen mode Exit fullscreen mode

Open/Closed Principle (OCP)

Entities (like classes, methods, or functions) should be open for extension but closed for modification. This means you can add new functionality without changing existing code.

Advantages:

  • Lower risk of introducing bugs
  • Faster development of new features since existing code remains unchanged
  • Enhanced reusability
class Salary {
  paySalary(employeeType: "intern" | "junior"): number {
    if (employeeType === "intern") return 100;
    if (employeeType === "junior") return 200;
    return 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

What if we want to add another type of employee? In this case we have to change existing code, that violates OCP.

To follow OCP, the implementation could be as below. In this case, when we want to add a new type of employee, we won't change any existing code.

interface Employee {
  paySalary(): number;
}

class Intern implements Employee {
  paySalary(): number {
    return 100;
  }
}

class Senior implements Employee {
  paySalary(): number {
    return 1000;
  }
}

class SalaryOCP {
  paySalary(employee: Employee): number {
    return employee.paySalary();
  }
}
Enter fullscreen mode Exit fullscreen mode

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types. In other words, wherever Parent is used, it could be replaced by Child without affecting the functionality of the program (without altering the existing code).

Advantages:

  • New classes can be added without breaking existing functionality
  • Enables creating substitutional parts of complex systems
  • Enhanced reusability
// Parent
class PaymentProcessor {
  public paymentProcess(): void {
   console.log("Payment was done");
  }
}

// Children
class CreditCard extends PaymentProcessor {
  public paymentProcess(): void {
   console.log("Paid with credit card");
  }
}

class DebitCard extends PaymentProcessor {
  public paymentProcess(): void {
   console.log("Paid with debit  card");
  }
}

class PayPal extends PaymentProcessor {
  public paymentProcess(): void {
   console.log("Paid with PayPal");
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use either Parent or its children in the function:

function makePayment(paymentProcessor: PaymentProcessor) {
  paymentProcessor.paymentProcess();
}

const paymentProcessor = new PaymentProcessor();
makePayment(paymentProcessor);

const creditCardProcessor = new CreditCard();
makePayment(creditCardProcessor);
Enter fullscreen mode Exit fullscreen mode

Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use. In other words, instead of adding new methods to an existing interface, create a new interface.

Advantages:

  • Smaller, more understandable interfaces
  • Changes in one interface do not impact unrelated classes
  • Enhanced reusability

Let's see at the example with violation of the principle.

interface Machine {
  print(): void;
  scan(): void;
  fax(): void;
}

class MultiFunctionPrinter implements Machine {
  print(): void {
    // actual implementation
  }
  scan(): void {
    // actual implementation
  }
  fax(): void {
    // actual implementation
  }
Enter fullscreen mode Exit fullscreen mode

What if we want to create a printer that will only print?

class SingleFunctionPrinter implements Machine {
  print(): void {
    // actual implementation
  }

  // We have to implement methods scan and fax
  scan(): void {
    // actual implementation
  }
  fax(): void {
    // actual implementation
  }
}
Enter fullscreen mode Exit fullscreen mode

The correct implementation will be as follows.

interface Printer {
  print(): void;
}

interface Scanner {
  print(): void;
}

interface FaxMachine {
  print(): void;
}

class SimplePrinter implements Printer {
  print(): void {
    // actual implementation
  }
}

class MultiFunctionMachine implements Printer, Scanner, FaxMachine {
  print(): void {
    // actual implementation
  }
  scan(): void {
    // actual implementation
  }
  fax(): void {
    // actual implementation
  }
}
Enter fullscreen mode Exit fullscreen mode

Dependency inversion principle (DIP)

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.

High-level modules - business logic, use cases
Low-level modules - writing to DB, handling HTTP requests

Abstractions - interfaces, abstract classes
Details - concrete classes

Advantages:

  • Promotes flexible and reusable code
  • Reduces coupling between different parts of the codebase
  • Enhanced reusability

Let's see at the example without DIP.

class MySQLDatabase {
  save(data: string): void {
    console.log("MySQLDatabase: ", data);
  }
}

class System {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  execute(data: string): void {
    this.database.save(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

And now at the example with DIP.

interface Database {
  save(data: string): void;
}

class MySQLDatabase implements Database {
  save(data: string): void {
    console.log("MySQLDatabase: ", data);
  }
}

class System {
  private database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  execute(data: string): void {
    this.database.save(data);
  }
}

const mySql = new MySQLDatabase();

const system = new System(mySql);

system.execute("test this");
Enter fullscreen mode Exit fullscreen mode

Top comments (0)