Návrhové vzory - 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
- Choose the right pattern: Don’t force a pattern where a simple solution works
- Keep it simple: Patterns should solve problems, not create complexity
- Test your implementations: Ensure patterns work correctly in your context
- Document your usage: Make it clear which patterns you’re using and why
- Refactor when needed: Be willing to change patterns as requirements evolve
Common Anti-Patterns to Avoid
- God Object: Classes that know too much or do too much
- Singleton Abuse: Using singletons for everything
- Tight Coupling: Classes that are too dependent on each other
- Inheritance Abuse: Deep inheritance hierarchies
- 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.