Skip to main content

Interfaces and depending on abstractions

Chapter 5 used an interface called NotificationChannel. This chapter digs into what interfaces are for.

What an interface is​

An interface defines only method names along with argument and return types, and carries no implementation. In PHP you define one with the interface keyword.

interface Logger
{
public function log(string $message): void;
}

This interface defines a contract: "there is a method named log, it takes a string, and it returns nothing." A class satisfies the contract by implementing it with implements.

class FileLogger implements Logger
{
public function log(string $message): void
{
// Write to a file (omitted)
}
}

The problem with depending on a concrete class​

Look at the OrderService class below.

❌ Bad: depending directly on a concrete class
class OrderService
{
private FileLogger $logger;

public function __construct()
{
$this->logger = new FileLogger();
}

public function placeOrder(Order $order): void
{
// Order processing (omitted)
$this->logger->log('Order accepted');
}
}

OrderService depends directly on the concrete class FileLogger. The moment you want to send logs somewhere other than a file — CloudWatch, or Slack — you have to rewrite the constructor of OrderService. Where logs go, which has nothing to do with order processing, has become a reason to change OrderService.

Depending on an abstraction​

Instead of the concrete FileLogger, make it depend on the Logger interface.

✅ Good: depending on an interface
class OrderService
{
public function __construct(
private readonly Logger $logger,
) {
}

public function placeOrder(Order $order): void
{
// Order processing (omitted)
$this->logger->log('Order accepted');
}
}

OrderService now depends only on the Logger contract, and does not know whether what it is handed is a FileLogger or some other implementation. Pass a CloudWatchLogger instead of a FileLogger and the log destination changes without touching any OrderService code.

The idea that "a higher-level class depends on an abstraction (an interface) rather than a concrete class" is called the dependency inversion principle (DIP)1. It is called "inversion" because it replaces the notion that a higher-level class (OrderService) depends directly on a lower-level one (FileLogger) with the arrangement that both depend on an abstraction (Logger).

The mechanism of handing the Logger implementation to OrderService from the outside, as a constructor argument, is called dependency injection. Chapter 11 covers it in detail alongside the Laravel container.

Summary​

  • An interface defines a "contract" that carries no implementation
  • Depending directly on a concrete class means rewriting the dependent class whenever you want to swap the implementation
  • Depending on an interface means not needing to know the implementation details, which makes swapping easy
  • This idea is called the dependency inversion principle; the mechanism that actually passes the dependency in from outside is called dependency injection, covered in chapter 11

The next chapter covers cohesion, coupling, and SOLID in review. As a wrap-up of the chapters so far, we revisit the principles we have seen against a common yardstick.

Exercises​

Rewrite the following PaymentProcessor class to depend on an interface
class StripeGateway
{
public function charge(int $amount): void
{
// Stripe API call (omitted)
}
}

class PaymentProcessor
{
private StripeGateway $gateway;

public function __construct()
{
$this->gateway = new StripeGateway();
}

public function process(int $amount): void
{
$this->gateway->charge($amount);
}
}

Sample answer

interface PaymentGateway
{
public function charge(int $amount): void;
}

class StripeGateway implements PaymentGateway
{
public function charge(int $amount): void
{
// Stripe API call (omitted)
}
}

class PaymentProcessor
{
public function __construct(
private readonly PaymentGateway $gateway,
) {
}

public function process(int $amount): void
{
$this->gateway->charge($amount);
}
}

PaymentProcessor depends only on the PaymentGateway contract. Switching payment providers away from Stripe means passing in a new class that implements PaymentGateway.


Footnotes​

  1. Source: Dependency inversion principle (Wikipedia). Robert C. Martin put the principle forward in 1996, defining it as "high-level modules should not depend on low-level modules; both should depend on abstractions." ↩