Skip to content
Mishel's Devnotes.
software development

Single Responsibility Principle (SRP) Explained: SOLID with Examples

In this article, we'll focus on the first principle: Single Responsibility Principle. The Single Responsibility Principle is commonly expressed as: A class should have one, and only one, reason to change.

Imagine you're developing an application that generates invoices for a business. Whenever a customer makes a purchase, the application needs to calculate the invoice amount, save the invoice, and send a confirmation email.

Since all these operations are related to invoices, you decide to create a class called InvoiceManager to handle everything.

Initially, this seems like a reasonable decision. The code works, the application is small, and there's no obvious reason to separate the operations.

A few weeks later, the finance team introduces a new requirement. Customers who purchase products worth more than a certain amount should receive a discount.

You open InvoiceManager and modify the calculation logic.

Later, the company decides to stop storing invoices in text files and migrate to a database. You open the same class again to modify the storage logic.

Then the customer support team requests changes to the confirmation email.

Once again, you're modifying InvoiceManager.

Notice what's happening?

The same class keeps changing for completely different reasons.

A change in the business's pricing policy requires modifying the class. A change in the storage mechanism requires modifying the same class. Even changing the email format requires modifying it.

Although all these operations are related to invoice processing, they don't necessarily belong to the same responsibility.

This is the kind of problem the Single Responsibility Principle (SRP) helps us recognize and solve.

What is SOLID?

Before understanding the Single Responsibility Principle, it's worth knowing where it comes from.

SOLID is a collection of five object-oriented design principles that help developers organize software so that it is easier to understand, maintain, test, and modify.

The acronym stands for:

LetterPrinciple
SSingle Responsibility Principle
OOpen/Closed Principle
LLiskov Substitution Principle
IInterface Segregation Principle
DDependency Inversion Principle

These principles are closely associated with software engineer Robert C. Martin, commonly known as Uncle Bob.

SOLID isn't a programming language, framework, or library. It's a set of design guidelines that can be applied across different programming languages and software architectures.

The principles are especially useful when developing applications that need to grow and change over time.

However, following SOLID doesn't mean making every application more complicated by introducing dozens of interfaces and classes. The purpose is to make software easier to change, not to increase the amount of code.

In this article, we'll focus on the first principle: Single Responsibility Principle.

What is the Single Responsibility Principle?

The Single Responsibility Principle is commonly expressed as:

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

At first, this definition sounds straightforward.

But what exactly is a reason to change?

Does it mean a class should contain only one method? Should it perform only one operation? Should every function have its own class?

No.

The Single Responsibility Principle isn't about counting methods or limiting how many lines of code a class can contain.

It's about identifying responsibilities that change independently of one another.

Let's return to our invoice application.

The calculation logic changes whenever the finance team introduces new pricing rules, discounts, or taxes.

The storage logic changes whenever the application needs a different storage mechanism or data format.

The notification logic changes whenever the company modifies its customer communication requirements.

Each of these changes has a different cause.

Robert C. Martin later described SRP in terms of actors. An actor, in this context, is a person or group of stakeholders responsible for a particular set of requirements.

The idea is that a module should be responsible to one actor or one coherent source of change.

When a class combines responsibilities controlled by different stakeholders, unrelated changes can become unnecessarily connected.

The Single Responsibility Principle encourages us to separate those responsibilities.

Understanding SRP with an example

Let's look at how an invoice-processing class might be implemented before applying SRP.

The application needs to perform three operations: calculate the invoice total, save the invoice to a file, and notify the customer.

We'll implement the same example in C#, Java, and Python.

The syntax will differ, but the design problem will remain the same.

Example: Implementation without SRP

InvoiceManager.cs
using System;
using System.IO;

public class InvoiceManager
{
    public void ProcessInvoice(
        decimal price,
        int quantity,
        string customerEmail)
    {
        // Calculate invoice amount
        decimal total = price * quantity;

        // Save invoice
        File.AppendAllText(
            "invoices.txt",
            $"Customer: {customerEmail}, Total: {total}"
                + Environment.NewLine
        );

        // Simulate email notification
        Console.WriteLine(
            $"Confirmation sent to {customerEmail}. Total: {total}"
        );
    }
}

public class Program
{
    public static void Main()
    {
        InvoiceManager manager = new InvoiceManager();

        manager.ProcessInvoice(
            500m,
            2,
            "customer@example.com"
        );
    }
}
InvoiceManager.java
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class Main {

    static class InvoiceManager {

        public void processInvoice(
                BigDecimal price,
                int quantity,
                String customerEmail) throws IOException {

            // Calculate invoice amount
            BigDecimal total = price.multiply(
                BigDecimal.valueOf(quantity)
            );

            // Save invoice
            String content = "Customer: " + customerEmail
                    + ", Total: " + total
                    + System.lineSeparator();

            Files.writeString(
                Path.of("invoices.txt"),
                content,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
            );

            // Simulate email notification
            System.out.println(
                "Confirmation sent to " + customerEmail
                + ". Total: " + total
            );
        }
    }

    public static void main(String[] args)
            throws IOException {

        InvoiceManager manager = new InvoiceManager();

        manager.processInvoice(
            new BigDecimal("500.00"),
            2,
            "customer@example.com"
        );
    }
}
InvoiceManager.py
from decimal import Decimal


class InvoiceManager:

    def process_invoice(self, price, quantity, customer_email):

        # Calculate invoice amount
        total = price * quantity

        # Save invoice
        with open("invoices.txt", "a", encoding="utf-8") as file:
            file.write(
                f"Customer: {customer_email}, Total: {total}\n"
            )

        # Simulate email notification
        print(
            f"Confirmation sent to {customer_email}. "
            f"Total: {total}"
        )


manager = InvoiceManager()

manager.process_invoice(
    Decimal("500.00"),
    2,
    "customer@example.com"
)

All three programs produce equivalent results.

Console output:

Confirmation sent to customer@example.com. Total: 1000.00

An invoice record is also appended to invoices.txt.

We're using decimal-based numeric types because invoice amounts represent money. C# provides decimal, Java provides BigDecimal, and Python provides Decimal through its decimal module.

The notification is only simulated. No actual email is sent.

What's wrong with these implementations?

There's nothing inherently wrong with the syntax of these programs. They accomplish the task we gave them.

The problem is how the responsibilities are organized.

Look at the InvoiceManager class.

It knows how invoice totals are calculated. It knows how invoices are stored. It also knows how customer notifications are generated.

Now imagine introducing a discount.

We need to modify InvoiceManager.

What if we want to replace the text file with a database?

We modify InvoiceManager again.

What if the customer confirmation message needs to include the invoice number?

Again, we modify the same class.

The class has become responsible for several concerns that can change independently.

This is an SRP violation.

You might think that splitting the three operations into separate methods inside InvoiceManager would solve the problem.

For example:

InvoiceManager
    |
    |-- CalculateTotal()
    |
    |-- SaveInvoice()
    |
    |-- SendConfirmation()

Although this might improve readability, it doesn't necessarily solve the design problem.

All three responsibilities are still owned by the same class.

To apply SRP, we need to think about which responsibilities should belong together and which should be separated.

Refactoring the invoice application using SRP

Let's redesign the application.

Instead of having one class responsible for everything, we'll create separate components.

InvoiceCalculator will calculate invoice totals.

InvoiceFileRepository will save invoice information.

InvoiceNotifier will handle customer notifications.

Finally, InvoiceProcessor will coordinate the overall workflow.

Our design will look like this:

                   InvoiceProcessor
                          |
             +------------+------------+
             |            |            |
             v            v            v
     InvoiceCalculator  InvoiceFile  InvoiceNotifier
                        Repository
             |            |            |
             v            v            v
        Calculates     Saves the     Sends customer
          total         invoice       confirmation

The important change is that each component owns a different responsibility.

Let's implement this design in all three programming languages.

C Sharp
using System;
using System.IO;

// Represents invoice data
public record Invoice(
    string CustomerEmail,
    decimal UnitPrice,
    int Quantity
);

// Responsibility: Calculate invoice totals
public class InvoiceCalculator
{
    public decimal CalculateTotal(Invoice invoice)
    {
        if (invoice.UnitPrice < 0 || invoice.Quantity <= 0)
        {
            throw new ArgumentException("Invalid invoice values.");
        }

        return invoice.UnitPrice * invoice.Quantity;
    }
}

// Responsibility: Save invoices
public class InvoiceFileRepository
{
    public void Save(Invoice invoice, decimal total)
    {
        string content =
            $"Customer: {invoice.CustomerEmail}, Total: {total}";

        File.AppendAllText(
            "invoices.txt",
            content + Environment.NewLine
        );
    }
}

// Responsibility: Notify customers
public class InvoiceNotifier
{
    public void SendConfirmation(Invoice invoice, decimal total)
    {
        Console.WriteLine(
            $"Confirmation sent to {invoice.CustomerEmail}. Total: {total}"
        );
    }
}

// Responsibility: Coordinate invoice processing
public class InvoiceProcessor
{
    private readonly InvoiceCalculator calculator;
    private readonly InvoiceFileRepository repository;
    private readonly InvoiceNotifier notifier;

    public InvoiceProcessor(
        InvoiceCalculator calculator,
        InvoiceFileRepository repository,
        InvoiceNotifier notifier)
    {
        this.calculator = calculator;
        this.repository = repository;
        this.notifier = notifier;
    }

    public void Process(Invoice invoice)
    {
        decimal total = calculator.CalculateTotal(invoice);

        repository.Save(invoice, total);

        notifier.SendConfirmation(invoice, total);
    }
}

public class Program
{
    public static void Main()
    {
        Invoice invoice = new(
            "customer@example.com",
            500m,
            2
        );

        InvoiceProcessor processor = new(
            new InvoiceCalculator(),
            new InvoiceFileRepository(),
            new InvoiceNotifier()
        );

        processor.Process(invoice);
    }
}
Java
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class Main {

    // Represents invoice data
    static class Invoice {
        final String customerEmail;
        final BigDecimal unitPrice;
        final int quantity;

        Invoice(String customerEmail,
                BigDecimal unitPrice,
                int quantity) {
            this.customerEmail = customerEmail;
            this.unitPrice = unitPrice;
            this.quantity = quantity;
        }
    }

    // Responsibility: Calculate invoice totals
    static class InvoiceCalculator {

        BigDecimal calculateTotal(Invoice invoice) {
            if (invoice.unitPrice.signum() < 0
                    || invoice.quantity <= 0) {
                throw new IllegalArgumentException(
                    "Invalid invoice values."
                );
            }

            return invoice.unitPrice.multiply(
                BigDecimal.valueOf(invoice.quantity)
            );
        }
    }

    // Responsibility: Save invoices
    static class InvoiceFileRepository {

        void save(Invoice invoice, BigDecimal total)
                throws IOException {

            String content =
                "Customer: " + invoice.customerEmail
                + ", Total: " + total
                + System.lineSeparator();

            Files.writeString(
                Path.of("invoices.txt"),
                content,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
            );
        }
    }

    // Responsibility: Notify customers
    static class InvoiceNotifier {

        void sendConfirmation(
                Invoice invoice,
                BigDecimal total) {

            System.out.println(
                "Confirmation sent to " + invoice.customerEmail
                + ". Total: " + total
            );
        }
    }

    // Responsibility: Coordinate invoice processing
    static class InvoiceProcessor {
        private final InvoiceCalculator calculator;
        private final InvoiceFileRepository repository;
        private final InvoiceNotifier notifier;

        InvoiceProcessor(
                InvoiceCalculator calculator,
                InvoiceFileRepository repository,
                InvoiceNotifier notifier) {
            this.calculator = calculator;
            this.repository = repository;
            this.notifier = notifier;
        }

        void process(Invoice invoice) throws IOException {
            BigDecimal total =
                calculator.calculateTotal(invoice);

            repository.save(invoice, total);

            notifier.sendConfirmation(invoice, total);
        }
    }

    public static void main(String[] args)
            throws IOException {

        Invoice invoice = new Invoice(
            "customer@example.com",
            new BigDecimal("500.00"),
            2
        );

        InvoiceProcessor processor = new InvoiceProcessor(
            new InvoiceCalculator(),
            new InvoiceFileRepository(),
            new InvoiceNotifier()
        );

        processor.process(invoice);
    }
}
Python
from dataclasses import dataclass
from decimal import Decimal


@dataclass
class Invoice:
    customer_email: str
    unit_price: Decimal
    quantity: int


# Responsibility: Calculate invoice totals
class InvoiceCalculator:

    def calculate_total(self, invoice):
        if invoice.unit_price < 0 or invoice.quantity <= 0:
            raise ValueError("Invalid invoice values.")

        return invoice.unit_price * invoice.quantity


# Responsibility: Save invoices
class InvoiceFileRepository:

    def save(self, invoice, total):
        with open("invoices.txt", "a", encoding="utf-8") as file:
            file.write(
                f"Customer: {invoice.customer_email}, "
                f"Total: {total}\n"
            )


# Responsibility: Notify customers
class InvoiceNotifier:

    def send_confirmation(self, invoice, total):
        print(
            f"Confirmation sent to {invoice.customer_email}. "
            f"Total: {total}"
        )


# Responsibility: Coordinate invoice processing
class InvoiceProcessor:

    def __init__(self, calculator, repository, notifier):
        self.calculator = calculator
        self.repository = repository
        self.notifier = notifier

    def process(self, invoice):
        total = self.calculator.calculate_total(invoice)

        self.repository.save(invoice, total)

        self.notifier.send_confirmation(invoice, total)


invoice = Invoice(
    customer_email="customer@example.com",
    unit_price=Decimal("500.00"),
    quantity=2
)

processor = InvoiceProcessor(
    InvoiceCalculator(),
    InvoiceFileRepository(),
    InvoiceNotifier()
)

processor.process(invoice)

All three implementations follow the same design.

Console output:

Confirmation sent to customer@example.com. Total: 1000.00

The invoice is also saved to a text file.

Notice something interesting.

The syntax differs across C#, Java, and Python, but the software design remains the same.

This is because SRP isn't tied to any particular programming language. It's about how we organize responsibilities.

Why is the new design better?

At first, the refactored application may look more complicated. We originally had one class, but now we have several.

Why would we write more code to accomplish the same task?

The answer becomes clearer when requirements change.

Suppose the business introduces a discount for orders worth more than 5,000.

We know exactly where to implement it: InvoiceCalculator.

We don't need to modify InvoiceFileRepository or InvoiceNotifier.

Now suppose we want to store invoices in a database instead of a text file.

We can replace or redesign the persistence component without changing the calculation rules.

Similarly, modifying the notification system doesn't require us to change invoice calculations.

This separation can also make collaboration easier. Developers working on unrelated features are less likely to need to modify the same class.

But there's an important detail here.

SRP doesn't guarantee that a change will affect only one file or one class.

Some requirements naturally affect multiple responsibilities. For example, adding a new invoice field may require changes to the data model, storage format, and notification template.

SRP helps reduce unnecessary coupling. It doesn't eliminate legitimate dependencies between components.

Does InvoiceProcessor violate SRP?

This is an interesting question.

Our InvoiceProcessor calls the calculator, repository, and notifier. Doesn't that mean it has three responsibilities?

Not necessarily.

The processor is responsible for coordinating the invoice-processing workflow.

Its job is to make sure the appropriate operations happen in the correct order.

It doesn't implement the calculation formula, write the file contents itself, or decide how notifications are delivered.

Consider a restaurant.

A waiter might take an order, communicate with the kitchen, and bring the prepared food to the table.

That doesn't mean the waiter is responsible for cooking the food, managing kitchen inventory, and preparing the bill.

The waiter coordinates parts of the service without taking ownership of every underlying responsibility.

Our InvoiceProcessor works in a similar way.

If the business changes the invoice-processing workflow, the processor may need to change. For example, invoices might require approval before being saved.

That's a change related to the processor's responsibility.

However, if the finance team changes the discount formula, the processor shouldn't need to know how the new calculation works.

This distinction between coordinating responsibilities and implementing unrelated responsibilities is useful when applying SRP.

Does SRP mean a class should have only one method?

No. This is one of the most common misunderstandings about the Single Responsibility Principle.

Let's consider a bank account.

A bank account needs to maintain a balance. Customers should be able to deposit and withdraw money.

We can represent these operations in one class.

C# example

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Invalid amount.");

        Balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0 || amount > Balance)
            throw new InvalidOperationException(
                "Invalid withdrawal."
            );

        Balance -= amount;
    }
}

Java example

import java.math.BigDecimal;

public class BankAccount {
    private BigDecimal balance = BigDecimal.ZERO;

    public BigDecimal getBalance() {
        return balance;
    }

    public void deposit(BigDecimal amount) {
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException(
                "Invalid amount."
            );
        }

        balance = balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        if (amount.signum() <= 0
                || amount.compareTo(balance) > 0) {
            throw new IllegalArgumentException(
                "Invalid withdrawal."
            );
        }

        balance = balance.subtract(amount);
    }
}

Python example

from decimal import Decimal


class BankAccount:

    def __init__(self):
        self._balance = Decimal("0")

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Invalid amount.")

        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0 or amount > self._balance:
            raise ValueError("Invalid withdrawal.")

        self._balance -= amount

Each class contains multiple methods, but does that mean it violates SRP?

No.

Both deposit and withdrawal operations are related to the same responsibility: managing the balance and basic rules of a bank account.

They belong together because they operate on the same account state and enforce related business rules.

Now imagine adding a method that sends promotional emails to customers.

That would introduce a different responsibility.

Marketing requirements can change without affecting deposit or withdrawal rules, so promotional communication would normally belong in another component.

The number of methods isn't the deciding factor. What matters is whether the methods belong to one cohesive responsibility.

These bank account classes are deliberately simplified. A production banking system would require additional concerns such as transaction records, concurrency control, authorization, and durable storage.

How SRP improves testing

Let's return to our invoice example.

Suppose we want to verify that an invoice containing two products priced at 500 each produces a total of 1,000.

In our original implementation, the calculation was part of InvoiceManager.

Calling the main method also wrote to a file and executed notification logic.

That makes testing less convenient because a simple calculation test involves unrelated operations.

After applying SRP, we can test the calculator independently.

The test only needs to create an invoice, call the calculation method, and compare the result with the expected value.

For example, the test follows this basic idea:

Arrange:
    Unit price = 500
    Quantity = 2

Act:
    Calculate invoice total

Assert:
    Total should be 1000

We don't need a database, file system, or email provider to verify the calculation.

The same benefit applies in C#, Java, and Python.

We could use xUnit or NUnit in C#, JUnit in Java, or pytest in Python.

The testing framework changes, but the underlying advantage remains the same.

A class with a focused responsibility is often easier to test because it has fewer unrelated dependencies.

This doesn't mean every class will be completely independent. Some classes need dependencies to perform their responsibilities.

It also doesn't remove the need for integration testing. We still need to verify that invoice processing works correctly when calculation, persistence, and notification components interact.

SRP simply makes it easier to test individual responsibilities separately.

SRP and dependency injection

Another concept that often appears alongside SRP is dependency injection.

In our invoice example, we created an InvoiceProcessor that receives a calculator, repository, and notifier through its constructor.

This is constructor dependency injection.

Rather than creating every dependency inside the processor, we provide the required objects from outside.

For example, the Python implementation contains:

processor = InvoiceProcessor(
    InvoiceCalculator(),
    InvoiceFileRepository(),
    InvoiceNotifier()
)

The processor receives the components it needs.

C# and Java follow the same idea in their respective implementations.

Dependency injection becomes particularly useful when we want to replace one implementation with another.

For example, we might replace InvoiceFileRepository with a database repository.

In a larger application, interfaces or other abstractions can make this replacement easier.

However, dependency injection isn't required by SRP.

SRP concerns how responsibilities are divided. Dependency injection concerns how objects receive the dependencies they need.

These concepts complement one another, but they solve different problems.

How to identify an SRP violation

Identifying responsibilities is often more difficult than writing the code.

It's relatively easy to look at a class that calculates salaries, saves files, and sends marketing emails and say that it has too many responsibilities.

Real applications aren't always that obvious.

For example, imagine a class called ProductService.

It calculates product discounts, validates selling prices, checks product availability, and updates stock quantities.

Should these operations belong to the same class?

The answer depends on the application.

Discount calculation and price validation may belong to a pricing responsibility.

Stock quantity updates may belong to inventory management.

Product availability might depend on inventory rules, reservations, and warehouse information.

If pricing and inventory are managed independently, separating these responsibilities may be sensible.

A useful question to ask is:

"What are the different reasons someone might ask me to change this class?"

If the reasons belong to unrelated areas of the business, it may be a sign that the class is handling multiple responsibilities.

You can also think about how developers work with the class.

If modifying a pricing rule requires someone to understand database connection code and email templates, the design may be unnecessarily coupled.

Another warning sign is testing. If a simple business rule cannot be tested without configuring unrelated systems, that may indicate that several concerns have been mixed together.

However, these are signs to investigate, not rules that automatically prove an SRP violation.

Responsibilities depend on the domain and requirements of the application.

Can SRP make code unnecessarily complicated?

Yes, if it's applied without considering the problem.

When developers first learn SOLID, they sometimes become so focused on separating responsibilities that they create a class for every small operation.

Imagine a simple application that calculates the area and perimeter of rectangles.

A rectangle class might have two methods: CalculateArea() and CalculatePerimeter().

Both methods describe behavior related to the rectangle's geometry.

Creating separate classes for each calculation may not provide any meaningful benefit.

The same issue can appear when every class is given an interface even though there is no useful abstraction or reason to introduce one.

You could end up with an application containing dozens of tiny classes that are difficult to navigate.

Instead of being easier to understand, the program becomes harder to follow because a simple operation requires jumping between several files.

This is why SRP should be treated as a design guideline.

The goal is to separate responsibilities that genuinely change for different reasons.

Sometimes that means introducing new classes.

Sometimes it means keeping several related methods together.

For a small application with stable requirements, a simple design may be perfectly reasonable.

As requirements evolve, responsibilities may become clearer, and refactoring can be introduced when it provides a practical benefit.

SRP vs separation of concerns

You might also encounter a related concept called Separation of Concerns.

Separation of Concerns is a broad software design principle that encourages developers to keep different concerns in distinct parts of an application.

For example, many applications separate presentation logic, business logic, and data access.

A web application might have controllers that handle HTTP requests, services that implement business workflows, and repositories that manage persistence.

SRP is more focused on the responsibilities of individual modules or classes and the reasons those modules need to change.

The two ideas are closely related, but they're not interchangeable.

An application can have separate presentation, business, and data-access layers while still containing classes with several unrelated responsibilities.

For example, putting every business operation into one enormous BusinessService class doesn't automatically result in good design.

The same applies to creating separate folders named Services, Repositories, and Models.

Folder structure alone doesn't determine whether a class follows SRP.

What matters is how responsibilities are defined and how changes affect the different parts of the system.

Final thoughts

The Single Responsibility Principle is easy to define but takes practice to apply properly.

At its heart is a simple idea: a class or module should have one coherent reason to change.

Our original invoice application contained one class responsible for calculating totals, saving invoices, and notifying customers.

All those operations were related to invoicing, but each could change for a different reason.

By separating them into focused components, we made the responsibilities easier to understand and modify.

We also saw that SRP doesn't mean every class should contain only one method. A bank account can reasonably support both deposits and withdrawals because those operations belong to the same responsibility.

And importantly, SRP isn't specific to C#, Java, or Python.

The same design considerations apply regardless of which of these languages you're using.

The next time you're creating a class, think about why it might need to change in the future.

If different teams or business requirements can independently force changes to unrelated parts of the class, consider whether some responsibilities should be separated.

That's where the Single Responsibility Principle becomes useful.


Practice questions

Question 1: What does the Single Responsibility Principle state?

Answer: The Single Responsibility Principle states that a class should have one, and only one, reason to change.

It encourages us to keep responsibilities that change for unrelated reasons in separate components.

Question 2: Does a class violate SRP if it contains five methods?

Answer: Not necessarily. A class can contain several methods as long as they support the same coherent responsibility.

For example, a stack implementation might contain methods such as push(), pop(), peek(), and isEmpty(). All these methods belong to the responsibility of managing a stack.

Question 3: Identify the problem in the following class.

EmployeeManager
    |
    |-- CalculateSalary()
    |
    |-- SaveEmployee()
    |
    |-- SendEmail()

Answer: The class combines salary calculation, employee persistence, and email communication.

These responsibilities may change independently.

A possible improvement would be to introduce separate components such as SalaryCalculator, EmployeeRepository, and EmployeeNotifier.

Question 4: Does this class follow SRP?

class Calculator:

    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

Answer: The class doesn't obviously violate SRP.

All three methods are related to arithmetic calculations and form a coherent responsibility.

Having multiple methods doesn't automatically mean a class has multiple responsibilities.

Question 5: Why is SRP useful for unit testing?

Answer: SRP can make unit testing easier because individual responsibilities are less likely to depend on unrelated systems.

For example, an invoice calculator can be tested without configuring database access or an email service.

Question 6: You're building a blogging application with a class called BlogManager. It creates blog posts, converts Markdown to HTML, saves posts to a database, sends subscriber notifications, and generates image thumbnails. Does this violate SRP?

Answer: It may.

Although all these operations are related to blogging, they represent concerns that can change independently.

A possible design would separate post management, Markdown rendering, persistence, notifications, and image processing into focused components.

A blog publishing service could then coordinate the necessary operations.

However, the exact design would depend on the application's requirements and complexity.

Question 7: Is creating a separate class for every method a good way to implement SRP?

Answer: No.

SRP is about separating responsibilities, not maximizing the number of classes.

Splitting closely related methods into unnecessary classes can make the code harder to understand and maintain.

Question 8: What is the difference between SRP and dependency injection?

Answer: SRP helps us decide how responsibilities should be divided between components.

Dependency injection allows components to receive the objects they depend on rather than creating those objects internally.

The two concepts often complement one another, but they address different design concerns.

Question 9: Can an application service that coordinates several components follow SRP?

Answer: Yes.

A service can coordinate several components while still having one coherent responsibility: managing a particular application workflow.

For example, an order-processing service may coordinate payment processing, inventory updates, and notifications without implementing all those operations itself.

Question 10: Explain the Single Responsibility Principle in your own words.

Sample answer: The Single Responsibility Principle means designing a class so that it focuses on one coherent responsibility. If different parts of the class need to change because of unrelated requirements, we should consider separating those responsibilities into different components. This can make the software easier to understand, test, and maintain.

Enjoyed this article? Explore more notes on engineering and the tools behind it.

Browse all articles