Skip to main content

Cohesion, coupling, and SOLID in review

The chapters so far have covered the single responsibility principle (chapter 4) and the dependency inversion principle (chapter 6). Those two are part of a set of five principles known as SOLID. This chapter fills in the remaining three (open-closed, Liskov substitution, interface segregation), and then revisits all five against a common yardstick.

The open-closed principle (OCP)​

As in chapter 5, we use payment methods as the subject. Branching on payment method with if statements looks like this.

❌ Bad: fixing the if statement every time a payment method is added
class PaymentProcessor
{
public function process(string $method, int $amount): void
{
if ($method === 'credit_card') {
// Credit card payment processing (omitted)
} elseif ($method === 'bank_transfer') {
// Bank transfer processing (omitted)
}
// This branch has to be edited every time a new payment method is added
}
}

Every new payment method means rewriting PaymentProcessor itself. Using the interface idea from chapter 6, it can be rewritten like this.

✅ Good: open for extension, closed for modification
interface PaymentMethod
{
public function process(int $amount): void;
}

class CreditCardPayment implements PaymentMethod
{
public function process(int $amount): void { /* Credit card payment processing (omitted) */ }
}

class PaymentProcessor
{
public function process(PaymentMethod $method, int $amount): void
{
$method->process($amount);
}
}

// Adding a new payment method does not change PaymentProcessor
class BankTransferPayment implements PaymentMethod
{
public function process(int $amount): void { /* Bank transfer processing (omitted) */ }
}

Adding a new payment method (BankTransferPayment) does not change a single line of PaymentProcessor. Being open to extension with new functionality while closed to modification of existing code is called the open-closed principle (OCP)1.

The Liskov substitution principle (LSP)​

Look at the classes below. They implement the NotificationChannel from chapter 5.

❌ Bad: an implementation that betrays the interface contract
interface NotificationChannel
{
public function send(string $message): void;
}

class EmailChannel implements NotificationChannel
{
public function send(string $message): void { /* Send email (omitted) */ }
}

class ReadOnlyChannel implements NotificationChannel
{
public function send(string $message): void
{
throw new RuntimeException('This notification channel cannot send');
}
}

ReadOnlyChannel implements NotificationChannel, but calling send always throws. Code that accepts a NotificationChannel ought to be able to assume it "can send," and passing in a ReadOnlyChannel breaks that assumption.

The principle that a derived type must not betray the behavior expected by users of the base type (here, the interface) is called the Liskov substitution principle (LSP)2. Classes implementing NotificationChannel should be limited to ones that really can send. If you want to represent a channel that cannot send, it belongs in a separate interface to begin with.

The interface segregation principle (ISP)​

Let us actually do that "separate interface."

❌ Bad: an interface that is too large
interface NotificationChannel
{
public function send(string $message): void;
public function receive(): array;
public function markAsRead(int $id): void;
}

class EmailChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }
public function receive(): array { /* ... */ }
public function markAsRead(int $id): void { /* ... */ }
}

class SmsChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }

public function receive(): array
{
throw new RuntimeException('The SMS channel does not support receiving');
}

public function markAsRead(int $id): void
{
throw new RuntimeException('The SMS channel does not support read tracking');
}
}

SmsChannel is forced to implement receive and markAsRead, which it does not use. That is also what causes the LSP violation just above: an implementation that throws from methods it cannot support.

✅ Good: splitting the interface along the lines its clients use
interface Sendable
{
public function send(string $message): void;
}

interface Receivable
{
public function receive(): array;
public function markAsRead(int $id): void;
}

class EmailChannel implements Sendable, Receivable
{
public function send(string $message): void { /* ... */ }
public function receive(): array { /* ... */ }
public function markAsRead(int $id): void { /* ... */ }
}

class SmsChannel implements Sendable
{
public function send(string $message): void { /* ... */ }
// No need to implement receive or markAsRead
}

SmsChannel now only has to implement Sendable, which is what it actually uses. The idea that a client should not be forced to depend on methods it does not use is called the interface segregation principle (ISP)3. Following ISP structurally avoids the LSP violation just above. That connection is not in the original write-up of ISP; it is this guide's framing.

Cohesion and coupling as a yardstick​

We have now seen all five SOLID principles: single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion. They can all be explained uniformly against two measures: cohesion and coupling4.

  • Cohesion: how strongly the elements gathered inside one class relate to each other. The higher the cohesion, the more focused the class's responsibility
  • Coupling: how strongly classes depend on each other. The lower the coupling, the less a change on one side affects the other

The single responsibility principle from chapter 4 is a principle for keeping cohesion high. The dependency inversion principle from chapter 6, along with OCP and ISP from this chapter, are principles for lowering coupling. LSP is the precondition that makes lowering coupling work at all. Even if depending on an abstraction lowers coupling, you cannot swap the dependency with confidence unless every class implementing that abstraction behaves as expected.

Summary​

  • OCP is the principle of being open to extension and closed to modification. It resolves branching by adding implementations
  • LSP is the principle that a derived type must not betray the behavior expected of the base type
  • ISP is the principle that a client should not be forced to depend on methods it does not use
  • The five SOLID principles come together as concrete means of keeping cohesion high and coupling low

Part 2 starts with the next chapter. We apply the principles so far to model and service class design in Laravel.

Exercises​

Say whether the following FileStorage classes violate LSP or ISP, and why
interface FileStorage
{
public function upload(string $path, string $content): void;
public function download(string $path): string;
public function delete(string $path): void;
}

class S3Storage implements FileStorage
{
public function upload(string $path, string $content): void { /* ... */ }
public function download(string $path): string { /* ... */ }
public function delete(string $path): void { /* ... */ }
}

class ReadOnlyArchiveStorage implements FileStorage
{
public function upload(string $path, string $content): void
{
throw new RuntimeException('Archive storage cannot be written to');
}

public function download(string $path): string { /* ... */ }

public function delete(string $path): void
{
throw new RuntimeException('Archive storage cannot be deleted from');
}
}

Sample answer

They violate both.

  • LSP violation: code that accepts a FileStorage can assume upload and delete work normally, but passing a ReadOnlyArchiveStorage throws and betrays that assumption.
  • ISP violation: ReadOnlyArchiveStorage is forced to implement upload and delete, which it does not (and cannot) use.

Split FileStorage into Readable (download only) and Writable (upload and delete only), and ReadOnlyArchiveStorage only has to implement Readable, clearing both violations at once.


Footnotes​

  1. Source: Open–closed principle (Wikipedia). Bertrand Meyer put it forward in his 1988 book Object-Oriented Software Construction. ↩

  2. Source: Liskov substitution principle (Wikipedia). It originates in an idea Barbara Liskov presented in a 1987 OOPSLA keynote, and Liskov and Jeannette Wing formalized it as a principle in a 1994 paper. Robert C. Martin introduced it widely in a 1996 article and popularized it. ↩

  3. Source: Interface segregation principle (Wikipedia). Robert C. Martin published the principle in a column in the November-December 1996 issue of C++ Report. ↩

  4. Source: Cohesion (computer science) / Coupling (computer programming) (Wikipedia). Larry Constantine devised the ideas in the late 1960s, and they were standardized in a 1974 paper and a 1979 book (Yourdon & Constantine). ↩