Single Responsibility: one reason to change
Keep calculation and persistence in separate roles so a change to storage does not affect pricing rules.
Watch it animatedStep through it, with the running line highlighted. Free with an account.The idea
Single Responsibility means a type has one cohesive responsibility and one main reason to change. InvoiceService coordinates the use case; InvoiceCalculator owns the pricing rule; IInvoiceRepository describes saving. It does not mean every class should have only one method.
Code
struct Invoice { double subtotal, taxRate; };
class InvoiceCalculator {
public:
double total(const Invoice& invoice) const {
return invoice.subtotal * (1 + invoice.taxRate);
}
};
class InvoiceRepository {
public:
virtual void save(const Invoice& invoice) = 0;
virtual ~InvoiceRepository() = default;
};
class InvoiceService {
InvoiceCalculator& calculator;
InvoiceRepository& repository;
public:
InvoiceService(InvoiceCalculator& calculator,
InvoiceRepository& repository)
: calculator(calculator), repository(repository) {}
double close(const Invoice& invoice) {
double total = calculator.total(invoice);
repository.save(invoice);
return total;
}
};from dataclasses import dataclass
from typing import Protocol
@dataclass
class Invoice:
subtotal: float
tax_rate: float
class InvoiceCalculator:
def total(self, invoice: Invoice) -> float:
return invoice.subtotal * (1 + invoice.tax_rate)
class InvoiceRepository(Protocol):
def save(self, invoice: Invoice) -> None: ...
class InvoiceService:
def __init__(self, calculator: InvoiceCalculator,
repository: InvoiceRepository):
self.calculator = calculator
self.repository = repository
def close(self, invoice: Invoice) -> float:
total = self.calculator.total(invoice)
self.repository.save(invoice)
return totalrecord Invoice(decimal Subtotal, decimal TaxRate);
class InvoiceCalculator {
public decimal Total(Invoice invoice) =>
invoice.Subtotal * (1 + invoice.TaxRate);
}
interface IInvoiceRepository {
void Save(Invoice invoice);
}
class InvoiceService {
private readonly InvoiceCalculator calculator;
private readonly IInvoiceRepository repository;
public InvoiceService(InvoiceCalculator calculator,
IInvoiceRepository repository) {
this.calculator = calculator;
this.repository = repository;
}
public decimal Close(Invoice invoice) {
var total = calculator.Total(invoice);
repository.Save(invoice);
return total;
}
}Tips
- A useful question is: “Which kind of change would make this type change?” Separate responsibilities when those changes are independent.
- Do not split every line into a class. A boundary is useful when it makes a real rule, concern, or dependency easier to change.
In the .NET library
SRP is about cohesive responsibilities, not class size. Here changing the tax formula belongs to InvoiceCalculator; changing database storage belongs to the repository implementation.