Most Popular Design Patterns

Throughout my experience, these design patterns have stood out to me.

Observer Pattern

It's useful for creating loose coupling between components, and enabling a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically without them needing to constantly poll for changes.

enum ProductEvent
{
	case Available;
	case Discounted;
}

interface ProductObserver
{
	function notify(Product $product, ProductEvent $event): void;
}

class Product
{
	/**
	 * @var ProductObserver[]
	 */
	private $observers = [];


	function changeAvailability()
	{
		// change availability

		$this->notify(ProductEvent::Available);
	}

	function setDiscount()
	{
		// set discount

		$this->notify(ProductEvent::Discounted);
	}

	// ----------- event code -----------

	function attach(ProductObserver $observer)
	{
		$this->observers[spl_object_hash($observer)] = $observer;
	}

	function detach(ProductObserver $observer)
	{
		unset($this->observers[spl_object_hash($observer)]);
	}

	function notify(ProductEvent $event)
	{
		foreach ($this->observers as $observer) {
			$observer->notify($this, $event);
		}
	}
}

class Emailer implements ProductObserver
{
	function notify(Product $product, ProductEvent $event): void
	{
		match ($event) {
			ProductEvent::Available => $this->sendProductAvailableEmail(),
			ProductEvent::Discounted => $this->sendProductDiscountedEmail(),
		};
	}

	function sendProductAvailableEmail()
	{
		echo 'Sending product available email' . PHP_EOL;
	}

	function sendProductDiscountedEmail()
	{
		echo 'Sending product discounted email' . PHP_EOL;
	}
}

class Logger implements ProductObserver
{
	function notify(Product $product, ProductEvent $event): void
	{
		echo 'Product updated: ' . $event->name  . PHP_EOL;
	}
}

$product = new Product;
$logger = new Logger;
$emailer = new Emailer;

$product->attach($logger);
$product->attach($emailer);

$product->changeAvailability();
$product->setDiscount();

$product->detach($emailer);

$product->changeAvailability();
$product->setDiscount();

Facade Pattern

The Facade design pattern provides a simplified, unified interface to a complex subsystem, hiding intricate interactions and making the system easier to use and modify. It reduces complexity by offering a clean, high-level entry point that encapsulates the underlying implementation details, promoting loose coupling and easier switching of inner implementations.

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

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

class DatabaseLogger implements Logger
{
	public function log(string $text): void
	{
		// log to a database
	}
}

class Log
{
	public static function info(string $logger, string $text): void
	{
		(new $logger)->log($text);
	}
}

Log::info(DatabaseLogger::class, 'hello');

Strategy Pattern

A way to switch implementations (strategies) for objects that perform the same actions differently.
  • When some action could be done in different ways, for example payment can be processed by different payment providers.
  • When passing a bunch of flags to a function as parameter to configure different behaviors just pass a different behavior (strategy).
interface PaymentStrategyInterface
{
	public function doPayment($amount): void;
}

class PaymentService
{
	public function __construct(private PaymentStrategyInterface $strategy) {}

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

class CreditCardPaymentStrategy implements PaymentStrategyInterface
{
	public function doPayment($amount): void
	{
		// do credit card stuff
	}
}

class PaypalPaymentStrategy implements PaymentStrategyInterface
{
	public function doPayment($amount): void
	{
		// do paypal stuff
	}
}

$ccStrategy = new CreditCardPaymentStrategy();
$paypalStrategy = new PaypalPaymentStrategy();

// pass in whichever
$paymentService = new PaymentService($ccStrategy);

// OR you can create Context, which encapsulates currently selected strategy

class PaymentStrategyContext
{
	private PaymentStrategyInterface $strategy;

	public function __construct(string $paymentMethod)
	{
		$this->strategy = match ($paymentMethod) {
			'paypal' => new PaypalPaymentStrategy(),
			'credit_card' => new CreditCardPaymentStrategy(),
			default => throw new \InvalidArgumentException('Unknown payment method'),
		};
	}

	public function doPayment($amount)
	{
		return $this->strategy->doPayment($amount);
	}
}

$strategyService = new PaymentStrategyContext($request->payment_method);
$strategyService->doPayment($amount);

Builder Pattern

The Builder pattern provides a flexible and readable way to construct complex objects step by step, separating the construction of a complex object from its representation. It allows you to create different variations of an object using the same construction process, which is particularly useful when an object has multiple optional parameters or configuration possibilities.

  • Creating objects with numerous constructor parameters
  • Implementing configurable object creation where the configuration may evolve
  • Providing a clean, fluent interface for object initialization
  • Ensuring consistent object creation across different configurations
class Book
{
	private $id;
	private $title;
	private $price;
	private $author;

	public function __construct(BookBuilder $builder)
	{
		$this->id = $builder->getId();
		$this->title = $builder->getTitle();
		$this->price = $builder->getPrice();
		$this->author = $builder->getAuthor();
	}
}

class BookBuilder
{
	private $id;
	private $title;
	private $price;
	private $author;

	public function setId($id): self
	{
		$this->id = $id;
		return $this;
	}

	public function setTitle($title): self
	{
		$this->title = $title;
		return $this;
	}

	public function setPrice($price): self
	{
		$this->price = $price;
		return $this;
	}

	public function setAuthor($author): self
	{
		$this->author = $author;
		return $this;
	}

	public function build()
	{
		if (empty($this->id) || empty($this->title) || empty($this->price)  || empty($this->author)) {
			throw new \Exception('Required fields missing');
		}

		return new Book($this);
	}
}

$productBuilder = new BookBuilder();
$product = $productBuilder
	->setId(101)
	->setTitle('Title')
	->setPrice(123.99)
	->setAuthor('author')
	->build();

Factory Pattern

Uniform way to create objects in one step. Factory is about "what" type of object to create.

interface Animal
{
	public function speak(): string;
}

class Dog implements Animal
{
	public function speak(): string
	{
		return 'Woof!';
	}
}

class Cat implements Animal
{
	public function speak(): string
	{
		return 'Meow!';
	}
}

class AnimalFactory
{
	public function createAnimal(string $type): Animal
	{
		return match ($type) {
			'dog' => new Dog(),
			'cat' => new Cat(),
			default => throw new \Exception('Unknown animal')
		};
	}
}

// Example usage
$factory = new AnimalFactory();
$dog = $factory->createAnimal('dog');
echo $dog->speak(); 

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by creating a wrapper that converts the interface of one class into another interface that clients expect. It enables seamless integration between classes with different interfaces, allowing you to make existing classes work with others without modifying their source code. This pattern is particularly useful when migrating between libraries, integrating third-party components, or bridging legacy and modern systems.

// target interface
interface BookInterface
{
	public function read();
}

// existing class with an incompatible interface
class EBook
{
	public function displayContent()
	{
		echo 'Displaying e-book content';
	}
}

class EBookAdapter implements BookInterface
{
	private $eBook;

	public function __construct(EBook $eBook)
	{
		$this->eBook = $eBook;
	}

	public function read()
	{
		$this->eBook->displayContent();
	}
}

class Library
{
	public function readBook(BookInterface $book)
	{
		$book->read();
	}
}

// Usage
$eBook = new EBook();
$eBookAdapter = new EBookAdapter($eBook);

$library = new Library();
$library->readBook($eBookAdapter);

Decorator Pattern

The Decorator pattern dynamically adds behaviors to an object by wrapping it with additional functionality, allowing flexible and granular extension without inheritance. It enables runtime modification of individual objects' capabilities without affecting other instances of the same class.

interface Pie
{
	public function makePie(): string;
	public function getPrice(): int;
}

class BasicPie implements Pie
{
	private const PRICE = 1;

	public function makePie(): string
	{
		return 'basic pie';
	}

	public function getPrice(): int
	{
		return self::PRICE;
	}
}

abstract class BasicPieDecorator implements Pie
{
	public function __construct(protected Pie $basicPie) {}

	abstract public function makePie(): string;
	abstract public function getPrice(): int;
}

class ApplePie extends BasicPieDecorator
{
	private const PRICE = 2;

	public function makePie(): string
	{
		return 'apple pie';
	}

	public function getPrice(): int
	{
		return $this->basicPie->getPrice() + self::PRICE;
	}
}

class PumpkinPie extends BasicPieDecorator
{
	private const PRICE = 3;

	public function makePie(): string
	{
		return 'pumpkin pie';
	}

	public function getPrice(): int
	{
		return $this->basicPie->getPrice() + self::PRICE;
	}
}

$basicPie = new BasicPie();
$basicPie->getPrice();

$applePie = new ApplePie($basicPie);
$applePie->getPrice();

$pumpkinPie = new PumpkinPie($basicPie);
$pumpkinPie->getPrice();

This article was updated on December 8, 2024