Design Patterns Explained
  • Getting Started
  • SOLID Principles
    • Single Responsibility Principle
    • Open / Closed Principle
    • Liskov Substitution Principle
    • Interface Segregation Principle
    • Dependency Inversion Principle
  • Design Patterns
    • Creational Patterns
      • Abstract Factory Pattern
      • Builder Pattern
      • Factory Pattern
      • Prototype Pattern
      • Singleton Pattern
    • Behavioral Patterns
      • Chain Of Responsibility Pattern
      • Command Pattern
      • Interpreter Pattern
      • Iterator Pattern
      • Mediator Pattern
        • Example 1
      • Memento Pattern
      • Observer Pattern
      • State Pattern
      • Strategy Pattern
        • Example 1
      • Template Method Pattern
      • Visitor Pattern
    • Structural Patterns
      • Adapter Pattern
      • Bridge Pattern
      • Composite Pattern
      • Decorator Pattern
      • Facade Pattern
      • Flyweight Pattern
      • Proxy Pattern
  • Roadmap
Powered by GitBook
On this page
  • Example
  • Solution

Was this helpful?

  1. SOLID Principles

Single Responsibility Principle

PreviousSOLID PrinciplesNextOpen / Closed Principle

Last updated 4 years ago

Was this helpful?

Single Responsibility Principle

Robert C. Martin's Definition: There should never be more than one reason for a class to change.

Wikipedia's Definition: Every module, class, or function should have responsibility for a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

  • Requirements changes typically map to responsibilities

  • More responsibilities in a class == The class is more likely to have changes

  • Having multiple responsibilities within a single class means that these responsibilities are coupled

Example

class Employee {
  private _salary: number;

  public constructor(salary: number) {
    this.salary = salary;
  }

  public set salary(value) {
    if (salary < 0) {
      throw new Error('Salary can not be negative');
    }

    this._salary = salary;
  }

  private getTaxes() {
    // some business logic for tax calculation
    return salary * 0.2;
  }

  public get netSalary() {
    return this._salary - this.getTaxes();
  }
}

Now let's see the problems introduced in this example:

  • The Validation logic for the salary was put inside the Employee class, so if there's a change to this validation logic the employee must be changed.

  • The business logic for tax calculation is also in the Employee class, so it will be very complex to put more than one tax calculation strategy or even change it as it's coupled with the Employee class.

  • Now the testing for Employee Unit will force you to test all of the three units together due to their coupling (The Employee itself, the Validation Logic, and the Tax Calculation logic).

Solution

class Employee {
  private _salary: number;
  public get salary() {
    return this._salary;
  }

  private taxCalculator: TaxCalculator;
  private validator: EmployeeValidator;

  public constructor(salary, taxCalculator, validator) {
    this.taxCalculator = taxCalculator;
    this.validator = validator;

    if (this.validator.isValidSalary(salary)) {
      this._salary = salary;
    } else {
      throw new Error('Invalid salary.');
    }
  }
}

class EmployeeValidator {
  public isValidSalary(salary: number) {
    if (salary < 0) {
      return false;
    }
    return true;
  }
}

class TaxCalculator {
  public getTaxes(salary: number) {
    return salary * 0.2;
  }
}

Now every piece of logic is decoupled, and the dependencies are explicit, the TaxCalculator and the Validator can now be treated as strategies that can be changed at anytime, they can even be automatically injected in the constructor or put to default values for simplicity.

As for the unit testing of those 3 modules, we can now test each unit separately to be sure which of them fails or succeeds.

Example UML
Solution UML