Design Patterns

Design Patterns Guide

This comprehensive guide covers essential design patterns and principles for software development, with practical examples and Drupal-specific implementations.

SOLID Principles in Drupal

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable.

1. Single-Responsibility Principle (SRP)

Definition: “There should never be more than one reason for a class to change” - Each class should have only one central responsibility.

Benefits in Drupal:

  • Easier testing: Each service can be tested independently
  • Better module organization: Clear separation of concerns
  • Reusability: Services can be reused in different contexts

Example:

// ❌ Violating SRP - Multiple responsibilities
class UserManager {
  public function createUser($userData) {
    // User creation logic
    $user = User::create($userData);
    $user->save();
    
    // Email sending logic
    $this->sendWelcomeEmail($user);
    
    // Logging logic
    \Drupal::logger('user')->info('User created');
    
    return $user;
  }
}

// ✅ Following SRP - Single responsibility per class
class UserCreationService {
  public function __construct(EmailServiceInterface $emailService) {
    $this->emailService = $emailService;
  }
  
  public function createUser($userData) {
    $user = User::create($userData);
    $user->save();
    
    $this->emailService->sendWelcomeEmail($user);
    return $user;
  }
}

2. Open-Closed Principle (OCP)

Definition: “Software entities should be open for extension, but closed for modification”

Benefits:

  • Extensibility without modifying existing code
  • Reduced risk of introducing bugs
  • Better maintainability

Example in Drupal:

// Using Drupal's Plugin System
abstract class PaymentMethodBase {
  abstract public function processPayment($amount, $currency);
  
  public function validatePayment($paymentData) {
    return $this->doValidation($paymentData);
  }
}

// Extension: Credit Card payment
class CreditCardPayment extends PaymentMethodBase {
  public function processPayment($amount, $currency) {
    return $this->chargeCreditCard($amount, $currency);
  }
}

3. Liskov Substitution Principle (LSP)

Definition: “Objects of a superclass should be replaceable with objects of its subclasses without breaking the application”

Key Requirements:

  • Preconditions cannot be strengthened
  • Postconditions cannot be weakened
  • Invariants must be preserved
  • Method signatures must match

Example:

interface ContentEntityInterface {
  public function getTitle();
  public function setTitle($title);
  public function isPublished();
}

class Node implements ContentEntityInterface {
  public function getTitle() {
    return $this->title ?? 'Untitled Node';
  }
  
  public function setTitle($title) {
    $this->title = $title;
    return $this;
  }
  
  public function isPublished() {
    return $this->published;
  }
}

4. Interface Segregation Principle (ISP)

Definition: “Clients should not be forced to depend upon interfaces that they do not use”

Benefits:

  • Smaller, focused interfaces
  • Reduced coupling
  • Easier testing and maintenance

Example:

// ❌ Fat interface
interface ContentManagerInterface {
  public function createContent($data);
  public function sendEmail($to, $subject, $body);
  public function logActivity($activity);
  public function generateReport($type);
}

// ✅ Segregated interfaces
interface ContentCrudInterface {
  public function createContent($data);
  public function updateContent($id, $data);
  public function deleteContent($id);
}

interface EmailSenderInterface {
  public function sendEmail($to, $subject, $body);
}

interface LoggerInterface {
  public function logActivity($activity);
}

5. Dependency Inversion Principle (DIP)

Definition: “Depend upon abstractions, not concretions”

Benefits:

  • Loose coupling between classes
  • Easier testing with dependency injection
  • Greater flexibility in implementation

Example:

interface OrderStorageInterface {
  public function saveOrder($orderData);
}

class OrderService {
  public function __construct(OrderStorageInterface $storage) {
    $this->storage = $storage;
  }
  
  public function processOrder($orderData) {
    $this->storage->saveOrder($orderData);
  }
}

Creational Patterns

Factory Method Pattern

Purpose: Define an interface for creating objects, but let subclasses decide which class to instantiate.

When to use:

  • When a caller can’t anticipate the types of objects it must create
  • When you have many objects of a common type
  • When you want to centralize object creation logic

Example:

interface LoggerFactoryInterface {
  public function createLogger(): LoggerInterface;
}

class FileLoggerFactory implements LoggerFactoryInterface {
  public function createLogger(): LoggerInterface {
    return new FileLogger();
  }
}

class DatabaseLoggerFactory implements LoggerFactoryInterface {
  public function createLogger(): LoggerInterface {
    return new DatabaseLogger();
  }
}

Builder Pattern

Purpose: Separate the construction of a complex object from its representation.

When to use:

  • When the algorithm for creating a complex object should be independent of the parts
  • When the construction process must allow different representations

Example:

class QueryBuilder {
  private $select = [];
  private $from;
  private $where = [];
  
  public function select($fields) {
    $this->select = is_array($fields) ? $fields : [$fields];
    return $this;
  }
  
  public function from($table) {
    $this->from = $table;
    return $this;
  }
  
  public function where($condition) {
    $this->where[] = $condition;
    return $this;
  }
  
  public function build() {
    $query = 'SELECT ' . implode(', ', $this->select);
    $query .= ' FROM ' . $this->from;
    if (!empty($this->where)) {
      $query .= ' WHERE ' . implode(' AND ', $this->where);
    }
    return $query;
  }
}

// Usage
$query = (new QueryBuilder())
  ->select(['id', 'name'])
  ->from('users')
  ->where('age > 18')
  ->build();

Singleton Pattern

Purpose: Ensure a class has only one instance and provide a global point of access to it.

When to use:

  • When there must be exactly one instance of a class
  • When the instance should be accessible globally

Example:

class DatabaseConnection {
  private static $instance = null;
  private $connection;
  
  private function __construct() {
    $this->connection = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
  }
  
  public static function getInstance() {
    if (self::$instance === null) {
      self::$instance = new self();
    }
    return self::$instance;
  }
  
  public function getConnection() {
    return $this->connection;
  }
}

Structural Patterns

Adapter Pattern

Purpose: Convert the interface of a class into another interface that clients expect.

When to use:

  • When you want to use an existing class with an incompatible interface
  • When you need to create a reusable class that cooperates with unrelated classes

Example:

interface PaymentProcessorInterface {
  public function processPayment($amount);
}

class StripePaymentProcessor {
  public function charge($amount, $currency = 'USD') {
    // Stripe-specific implementation
  }
}

class StripeAdapter implements PaymentProcessorInterface {
  private $stripeProcessor;
  
  public function __construct(StripePaymentProcessor $processor) {
    $this->stripeProcessor = $processor;
  }
  
  public function processPayment($amount) {
    return $this->stripeProcessor->charge($amount);
  }
}

Composite Pattern

Purpose: Compose objects into tree structures to represent part-whole hierarchies.

When to use:

  • When you want to represent part-whole hierarchies of objects
  • When clients should treat individual objects and compositions uniformly

Example:

interface ComponentInterface {
  public function render(): string;
}

class Leaf implements ComponentInterface {
  private $content;
  
  public function __construct($content) {
    $this->content = $content;
  }
  
  public function render(): string {
    return $this->content;
  }
}

class Composite implements ComponentInterface {
  private $children = [];
  
  public function add(ComponentInterface $component) {
    $this->children[] = $component;
  }
  
  public function render(): string {
    $output = '';
    foreach ($this->children as $child) {
      $output .= $child->render();
    }
    return $output;
  }
}

Observer Pattern

Purpose: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.

When to use:

  • When a change to one object requires changing others
  • When an object should be able to notify other objects without knowing who they are

Example:

interface ObserverInterface {
  public function update($data);
}

interface SubjectInterface {
  public function attach(ObserverInterface $observer);
  public function detach(ObserverInterface $observer);
  public function notify();
}

class UserRepository implements SubjectInterface {
  private $observers = [];
  private $users = [];
  
  public function attach(ObserverInterface $observer) {
    $this->observers[] = $observer;
  }
  
  public function createUser($userData) {
    $this->users[] = $userData;
    $this->notify();
  }
  
  public function notify() {
    foreach ($this->observers as $observer) {
      $observer->update($this->users);
    }
  }
}

class UserLogger implements ObserverInterface {
  public function update($data) {
    // Log user creation
    \Drupal::logger('user')->info('User list updated');
  }
}

Behavioral Patterns

Strategy Pattern

Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable.

When to use:

  • When you have multiple ways to perform an operation
  • When you need to select an algorithm at runtime

Example:

interface SortingStrategyInterface {
  public function sort(array $data): array;
}

class BubbleSortStrategy implements SortingStrategyInterface {
  public function sort(array $data): array {
    // Bubble sort implementation
    return $data;
  }
}

class QuickSortStrategy implements SortingStrategyInterface {
  public function sort(array $data): array {
    // Quick sort implementation
    return $data;
  }
}

class Sorter {
  private $strategy;
  
  public function setStrategy(SortingStrategyInterface $strategy) {
    $this->strategy = $strategy;
  }
  
  public function sort(array $data): array {
    return $this->strategy->sort($data);
  }
}

State Pattern

Purpose: Allow an object to alter its behavior when its internal state changes.

When to use:

  • When an object’s behavior depends on its state
  • When operations have large, multipart conditional statements

Example:

interface OrderStateInterface {
  public function process(Order $order);
  public function cancel(Order $order);
}

class PendingState implements OrderStateInterface {
  public function process(Order $order) {
    // Process pending order
    $order->setState(new ProcessingState());
  }
  
  public function cancel(Order $order) {
    $order->setState(new CancelledState());
  }
}

class ProcessingState implements OrderStateInterface {
  public function process(Order $order) {
    $order->setState(new CompletedState());
  }
  
  public function cancel(Order $order) {
    // Cannot cancel processing order
  }
}

class Order {
  private $state;
  
  public function __construct() {
    $this->state = new PendingState();
  }
  
  public function setState(OrderStateInterface $state) {
    $this->state = $state;
  }
  
  public function process() {
    $this->state->process($this);
  }
  
  public function cancel() {
    $this->state->cancel($this);
  }
}

Drupal-Specific Patterns

Plugin Pattern

Purpose: Allow modules to provide extensible functionality through plugins.

Example:

/**
 * @Block(
 *   id = "custom_block",
 *   admin_label = @Translation("Custom Block"),
 *   category = @Translation("Custom")
 * )
 */
class CustomBlock extends BlockBase {
  public function build() {
    return [
      '#markup' => $this->t('Hello from custom block!'),
    ];
  }
}

Service Pattern

Purpose: Provide centralized services that can be injected into other classes.

Example (services.yml):

services:
  mymodule.custom_service:
    class: Drupal\mymodule\Service\CustomService
    arguments: ['@database', '@current_user']

Hook Pattern

Purpose: Allow modules to alter or extend Drupal’s behavior.

Example:

function mymodule_node_presave(NodeInterface $node) {
  if ($node->getType() == 'article') {
    $node->setTitle(strtoupper($node->getTitle()));
  }
}

Best Practices

  1. Choose the right pattern: Don’t force a pattern where a simple solution works
  2. Keep it simple: Patterns should solve problems, not create complexity
  3. Test your implementations: Ensure patterns work correctly in your context
  4. Document your usage: Make it clear which patterns you’re using and why
  5. Refactor when needed: Be willing to change patterns as requirements evolve

Common Anti-Patterns to Avoid

  1. God Object: Classes that know too much or do too much
  2. Singleton Abuse: Using singletons for everything
  3. Tight Coupling: Classes that are too dependent on each other
  4. Inheritance Abuse: Deep inheritance hierarchies
  5. Over-engineering: Using patterns when they’re not needed

This guide provides a foundation for understanding and applying design patterns in software development, with particular emphasis on Drupal implementations.