Single Responsibility Principle

·

3 min read

Single Responsibility Principle

REPO Linkedin Github

Info : Single-responsibility principle - Wikipedia

The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented programming. It states that a class should have only one reason to change. In other words, a class should have only one responsibility or job.

node dist/srp.js

Each class should focus on a single responsibility or task.

Example: E-commerce Order System

  • Imagine an e-commerce platform with an order processing system.

  • The system handles various tasks:

    • Product management

    • Order creation

    • Pricing calculation

    • Invoice generation

    • Payment processing

|---srp.ts 
|---order
    |---Order.ts
    |---Product.ts   
    |---Jobs
         |---invoice.ts
         |---PaymentProcessor.ts
         |---PricingCalculator.ts

Breaking Down Responsibilities

  • Product Class (Product.ts):

    • Responsible for representing product details (ID, name, price).
  • Order Class (Order.ts):

    • Manages the list of products in an order.

    • Adds and retrieves products.

  • Invoice Class (invoice.ts):

    • Generates an invoice for an order.

    • Displays product names and prices.

  • PaymentProcessor Class (PaymentProcessor.ts):

    • Handles payment processing.

    • Sends emails and updates accounting.

  • PricingCalculator Class (PricingCalculator.ts):

    • Calculates the total price of an order.

order/Product.ts

export class Product {

    constructor(id: string, name: string, price: number) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    id : string;
    name : string;
    price : number;
}

order/Order.ts

import {Product} from "./Product";
export class Order {
    product: Product [] = []

    addProduct(product: Product) {
        this.product.push(product)
    }

    getProduct() {
        return this.product
    }
}

order/Jobs/invoice.ts

import {Product} from "../Product";

export class Invoice {

    generateInvoice(product: Product[] , amount: number) {
        console.log(`
        Invoice Date : ${new Date().toLocaleString()}
        _____________________________

        Product Name\t\t\tPrice
        `);

        product.forEach((product:Product)=> {
            console.log(`${product.name}\t\t\t${product.price}`)
        });

        console.log(`_____________________________`);
        console.log(`Total Price : ${amount}`)
    }
}

order/Jobs/PaymentProcessor.ts

import {Order} from "../Order";

export class PaymentProcessor {
    processPayment(order: Order) {
        console.log(`Processing payment...`)
        console.log(`Payment processed successfully.`)
        console.log(`Added to accounting system!`)
        console.log(`Email sent to customer!`)
    }
}

order/Jobs/PricingCalculator.ts

import {Product} from "../Product";

export class PricingCalculator {
    calculatePricing(products: Product[]): number {
        return products.reduce((acc, product) => acc + product.price, 0);
    }
}

Exceptions and Violations

  • Exceptions:

    • Sometimes, combining responsibilities is necessary for efficiency.

    • For example, tightly coupling pricing calculation and order management might be acceptable.

  • Violations:

      // Product class representing product details
      class Product {
          constructor(public id: string, public name: string, public price: number) {}
      }
    
      // Imagine an Order class that handles both order management and payment processing
      class Order {
          private orderID: string;
          private products: Product[];
    
          constructor(orderID: string) {
              this.orderID = orderID;
              this.products = [];
          }
    
          addProduct(product: Product) {
              this.products.push(product);
          }
    
          calculateTotalPrice(): number {
              return this.products.reduce((total, product) => total + product.price, 0);
          }
    
          processPayment(paymentMethod: string) {
              // Process payment logic here
              console.log(`Payment for order ${this.orderID} processed via ${paymentMethod}`);
          }
      }
    
      // Usage
      const product1 = new Product("1", "Laptop", 200000);
      const product2 = new Product("2", "Phone", 60000);
    
      const order = new Order("123");
      order.addProduct(product1);
      order.addProduct(product2);
    
      const total = order.calculateTotalPrice();
      console.log(`Total price: ${total}`);
    
      order.processPayment("Credit Card");
    
    • The Order class combines two distinct responsibilities: managing the list of products (order management) and processing payments.

    • Violation: If payment processing logic changes, it impacts the Order class, which should focus only on order management.

    • Solution: Separate payment processing into a dedicated class (e.g., PaymentProcessor). Each class should have a clear purpose to adhere to SRP.

srp.ts

import {Product, Order, PricingCalculator, Invoice ,PaymentProcessor} from "./order";

const product1 = new Product("1","Laptop", 200000);
const product2 = new Product("2","Phone", 60000);
const product3 = new Product("3", "Car", 8000000);

const order = new Order();

order.addProduct(product1);
order.addProduct(product2);
order.addProduct(product3);

const pricingCalculator = new PricingCalculator();
const total = pricingCalculator.calculatePricing(order.getProduct());

const invoice = new Invoice();
invoice.generateInvoice(order.getProduct(), total);

const paymentProcessor = new PaymentProcessor();
paymentProcessor.processPayment(order);

Summary

  • SRP ensures that each class has a clear purpose.

  • By separating concerns, we improve maintainability and reduce the impact of changes.

  • In our e-commerce example, adhering to SRP leads to a more robust and flexible system.