Dive Into SOLID: The Single Responsibility Principle

By Cristian Araya | Design | September 2, 2024

The history of SOLID officially began in the early 2000s. Robert C. Martin first presented the principles in his book "Agile Software Development: Principles, Patterns and Practices," though not in the order we know them today. In 2004, Michael Feathers suggested to Martin that rearranging the principles would make their first letters spell "SOLID" — and that's how Uncle Bob presented them in his book "Clean Architecture."

Putting the fun fact aside, the "S" in SOLID might be the most crucial of the five principles. It's the easiest to grasp and can have the most immediate impact on your code. So, without further ado, let's dive into The Single Responsibility Principle (SRP).

The SRP is commonly defined as:

"A module should have one, and only one, reason to change."

However, the original definition in "Agile Software Development" referred to a class, not a module. Martin later updated this in "Clean Architecture." This change broadens the scope of SOLID principles, showing they're not limited to object-oriented programming. Instead, they can be applied at the module level or "mid-level" of any programming paradigm.

The SRP is often misunderstood as "A function should do one, and only one, thing" — a principle Uncle Bob presented in his book "Clean Code." However, these concepts, while complementary, are not identical. The SRP focuses on "a reason to change," meaning a module can perform multiple functions, but all these functions should change for the same reason. This subtle distinction allows for more flexible module design while maintaining a single responsibility.

Martin then refined the Single Responsibility Principle, rephrasing it as:

"A module should be responsible to one, and only one, actor."

An "actor" in this context refers to a person or group who might request a change in the software. This refined definition has a profound impact on software development. The SRP suggest that all code we write is influenced by these actors. To create high-quality, cost-effective software, it's crucial to identify these actors and the modules responsible for them.

/images/srp.webp

Let's examine how this principle looks in code. This GitHub repository implements the SOLID principles. The project serves as middleware between an e-commerce platform and an ERP system, processing customer orders. While the e-commerce platform sells items in packs, the ERP system only handles single items. The middleware's role is to break down these item packs into their corresponding individual items for the ERP.

So, we just have to inject the order. Easy, right? At first, yes. But as development progressed, "strange" requirements began to arrive. Suddenly, your repository has files with more than 100 lines of code, and one feature change affects more files than it should. Fortunately, the Single Responsibility Principle can help us reduce the line of code, the number of files that need changing and, more importantly, decrease the time required to implement changes.

Let's consider the actors involved—those who might request changes in the code. We can get them from the e-commerce and ERP orders structures:

/* E-commerce Order */

{
  "id": "ABCDEFGHI12",
  "customer": {
    "id": "12-1234567",
    "firstName": "John",
    "lastName": "Doe",
    "email": "johndoe@email.com"
  },
  "dispatch": {
    "name": "Jane Doe",
    "phone": "1-123-123-1234",
    "email": "janedoe@email.com",
    "state": "Texas",
    "street": "1234 Whitis Ave",
    "zipCode": "2345-1234"
  },
  "products": [
    {
      "id": "a5c2bfd9-38a1-48a9-b7d9-3a31c36c4d78",
      "name": "Pack",
      "price": 59.99,
      "quantity": 2,
      "total": 119.98,
      "properties": {
        "externalId": "ABCDFG",
        "packNumberOfItems": "6"
      }
    }
  ],
  "total": 119.98,
  "tax": 8.25
}
/* ERP Order */

{
  "number": "ABCDEFGHI12",
  "customer": {
    "tin": "12-1234567",
    "name": "John Doe",
    "email": "johndoe@email.com"
  },
  "dispatch": {
    "name": "Jane Doe",
    "phone": "123-123-1234",
    "email": "janedoe@email.com",
    "address": "1234 Whitis Av, Austin, Texas, 12345-1234"
  },
  "products": [
    {
      "code": "ABCDFG",
      "price": 59.99,
      "quantity": 2,
      "totalPrice": 119.98
    }
  ],
  "taxRate": 8.25,
  "total": 119.98
}

We quickly realize we can separate the order into four modules: dispatch, products, customer, and payment. Each component corresponds to a different department: logistics handles dispatch, inventory manages products, customer service deals with customer data, and finance oversees payments.

By breaking down the responsibilities within the order into these four distinct groups, we arrive at the following architecture:

/images/solid architecture.webp

With the structure of an e-commerce order and an ERP order as follow:

// order-jector/interfaces/order.ts

export interface EcommerceOrder {
  id: string
  customer: EcommerceCustomer
  dispatch: EcommerceDispatch
  products: EcommercePack[]
  total: number
  tax: number
}

export interface ErpOrder extends ErpPayment {
  customer: ErpCustomer
  dispatch: ErpDispatch
  items: ErpItem[]
}

The Order Injector module will be the place where the ERP order is built and injected.

// order-injector/services/transform-order.ts

import { EcommerceOrder, ErpOrder } from "../interfaces/order"
import transformToErpPayment from "@/payment-transformer/services/transform-to-erp-payment"
import transformToErpItems from "@/product-transformer/services/transform-to-erp-items"
import transformtoErpDispatch from "@/dispatch-transformer/services/transform-to-erp-dispatch"
import transformtoErpCustomer from "@/customer-transformer/services/transform-dispatch"

export default function buildErpOrder(order: EcommerceOrder): ErpOrder {
  const items = transformToErpItems(order.products)
  const dispatch = transformtoErpDispatch(order.dispatch)
  const customer = transformtoErpCustomer(order.customer)
  const payment = transformToErpPayment(order)
  const erpOrder: ErpOrder = { number: order.id, items, dispatch, customer, ...payment }
  return erpOrder
}

// order-injector/services/inject-order.ts

import { EcommerceOrder } from "../interfaces/order"
import { ErpOrderFetcher } from "../interfaces/fetcher"
import buildErpOrder from "./build-erp-order"

export default async function injectOrder(order: EcommerceOrder, fetcher: ErpOrderFetcher) {
  const erpOrder = buildErpOrder(order)
  await fetcher(erpOrder)
}

This approach results in a concise module with few lines of code, significantly reducing the scope of changes needed when new requirements arise.

Now, let's say a change in the payment data has arrived. Currently, the ERP payment transformer looks like this:

// payment-transformer/services/transform-to-erp-payment.ts

export default function transformToErpPayment(invoice: EcommercePayment): ErpPayment {
  validateSchema<EcommercePayment>(ecommercePaymentSchema, invoice)
  return {
    number: invoice.id,
    taxRate: invoice.tax,
    total: calculatePriceWithTax(invoice.total, invoice.tax)
  }
}

Now the finances department need to add the tax amount. To do it we only need to modify the ErpPayment interface and the transformer, leaving the others modules intact:

// payment-transformer/interfaces/payment.ts

export interface ErpPayment {
  number: string
  taxRate: number
  taxAmount: number
  total: number
}
export default function transformToErpPayment(invoice: EcommercePayment): ErpPayment {
  validateSchema<EcommercePayment>(ecommercePaymentSchema, invoice)
  return {
    number: invoice.id,
    taxRate: invoice.tax,
    taxAmount: calculateTaxAmount(invoice.total, invoice.tax),
    total: calculatePriceWithTax(invoice.total, invoice.tax)
  }
}

The Single Responsibility Principle strives to reduce coupling while increasing cohesion, but it cannot eliminate coupling entirely. That's why the Order Injector serves as the central point for calling other modules. Some changes will inevitably impact multiple modules due to these dependencies. For such situations, principles like Open-Closed and Dependency Inversion come into play, but we'll explore those in other posts in this series.

Conclusion

The Single Responsibility Principle (SRP) is a powerful tool that can significantly reduce the time needed to implement changes in our code. This is achieved by separating responsibilities into cohesive modules. To identify these modules, we must examine the code with the actors in mind—those who will drive new changes. By doing so, we reduce coupling between modules, minimizing the effort required to modify the code.

Related Posts