The single responsibility principle
The Order class we saw in chapter 1 was a god class with six roles: managing order lines, calculating the total, checking stock, sending the confirmation email, saving to the database, and generating the invoice PDF.
In this chapter we actually split it up.
Signs of a god class
Before splitting anything, let us pin down how to spot a god class. The following traits are signs that a god class is worth suspecting.
- The constructor or the methods take a lot of arguments
- The class name is a generic word like "Manager" or "Helper," or contains several nouns
- The class deals with many kinds of concern (persisting data, notifying, calculating amounts, generating external formats, and so on)
- You cannot explain in one sentence why you would change the class
The Order class from chapter 1 matched that last item.
"Because the order lines changed," "because the email copy changed," "because the stock table structure changed," "because the invoice format changed" — four or five distinct reasons to change it.
What the single responsibility principle says
The idea that a class should have only one reason to change is called the single responsibility principle (SRP)1. It was put forward by Robert C. Martin.
The "reason" here is a question of who changes the class, and on whose account.
For the Order class, the reasons to change break down by area of responsibility.
- How order lines are held changes: a specification change to the order itself
- The email copy changes: something marketing or customer support wants
- The stock table structure changes: something the inventory system wants
- The invoice format changes: something accounting wants
These reasons have nothing to do with each other. While they live together in one class, a change made for one reason permanently carries the risk of affecting unrelated processing.
Splitting the Order class
As promised in chapter 1, we split it up by responsibility.
class Order
{
private array $items = [];
public function __construct(
private readonly string $customerEmail,
) {
}
public function addItem(string $name, int $price, int $quantity): void
{
$this->items[] = [
'name' => $name,
'price' => $price,
'quantity' => $quantity,
];
}
public function calculateTotal(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
public function getItems(): array
{
return $this->items;
}
public function getCustomerEmail(): string
{
return $this->customerEmail;
}
}
class StockChecker
{
public function hasEnoughStock(Order $order): bool
{
foreach ($order->getItems() as $item) {
$stock = Database::query(
'SELECT quantity FROM stocks WHERE name = ?',
[$item['name']]
);
if ($stock < $item['quantity']) {
return false;
}
}
return true;
}
}
class OrderMailer
{
public function sendConfirmation(Order $order): void
{
$body = "Thank you for your order. Your total is {$order->calculateTotal()} yen.";
mail($order->getCustomerEmail(), 'Order confirmation', $body);
}
}
class InvoicePdfGenerator
{
public function generate(Order $order): string
{
$total = $order->calculateTotal();
// PDF generation (omitted; $total goes into the invoice total field)
return "invoice-{$order->getCustomerEmail()}.pdf";
}
}
What remains in the Order class is only the responsibility of the order itself: holding order lines and calculating the total.
Checking stock, sending mail, and generating the PDF have each moved to their own class.
Saving to the database is not covered in this chapter.
Persistence design is taken up again in Part 2, the practical Laravel half.
What splitting reveals
There is one thing worth being honest about.
This split did not, in itself, reduce the number of places that depend on calculateTotal.
Both OrderMailer and InvoicePdfGenerator still call $order->calculateTotal().
Change the shipping fee logic and you still have to check both places.
The split tidied the classes up, but the coupling — the dependency on calculateTotal — is still there.
What the split did change is that each class now carries a single concern.
When you want to change the stock check logic, reading StockChecker is enough.
When you want to change the email copy, you look at OrderMailer only, with no need to skim past stock-check or PDF-generation code.
And when you want to reuse StockChecker on another screen, you can call it on its own instead of dragging the whole Order class along.
Designs that reduce dependency on a shared value like calculateTotal, referenced from several classes, are covered in later chapters.
Summary
- You can spot a god class by how many arguments its constructor and methods take, and by how hard it is to explain why you would change it
- The single responsibility principle is the idea of narrowing a class down to one reason to change
- Splitting by responsibility narrows each class down to a single concern, so there is less to read when you make a change
- Splitting does not reduce the dependencies themselves; reducing dependency on a value takes a different technique
What to read next
The next chapter covers inheritance and composition. We look at is-a and has-a relationships, and at why composition is preferred over inheritance.
Exercises
Name three reasons the following ReportGenerator class would have to change
class ReportGenerator
{
public function generate(array $salesData): void
{
// Aggregation
$total = array_sum(array_column($salesData, 'amount'));
// Convert to CSV
$csv = "date,amount\n";
foreach ($salesData as $row) {
$csv .= "{$row['date']},{$row['amount']}\n";
}
// Save to a file
file_put_contents('/tmp/report.csv', $csv);
// Notify Slack
$this->postToSlack("Report generated. Total: {$total} yen");
}
private function postToSlack(string $message): void
{
// Slack API call (omitted)
}
}
Sample answer
At least the following three reasons to change are visible.
- The aggregation logic changes (adding a metric other than the total, say — something the reporting side wants)
- The output format changes (switching from CSV to JSON, say — something the downstream system wants)
- The notification method changes (switching from Slack to email, say — something the notification infrastructure wants)
Four processes — aggregating, converting the format, saving, and notifying — are living together in a single method.
Just like the Order class, each can be split into its own class.