Návrhové vzory (Design Patterns)
Co tato dovednost pokrývá
Principy SOLID a klasické tvořivé (creational), strukturální (structural) a behaviorální (behavioral) návrhové vzory, každý doložený konkrétním příkladem v PHP — včetně specifických implementací pro Drupal (Plugin, Service, Hook).
Kdy ji použít
- Navrhujete novou Drupal službu nebo modul a chcete si ji před psaním kódu ověřit vůči SOLID
- Řešíte, jak pružně vytvářet objekty (Factory Method, Builder, Singleton) místo natvrdo zapsaného
new - Napojujete existující třídu s nekompatibilním rozhraním do svého kódu, např. adaptujete externí SDK (Adapter)
- Modelujete hierarchie typu část-celek, například strom renderovatelných komponent (Composite)
- Potřebujete upozornit více posluchačů na změnu stavu objektu, aniž byste je pevně provázali (Observer)
- Volíte algoritmus nebo strategii za běhu místo větvení podle typu (Strategy)
- Modelujete objekt, jehož chování se mění podle vnitřního stavu, např. životní cyklus objednávky (State)
- Vytváříte vlastní typ Drupal pluginu, injektovatelnou službu nebo implementaci hooku
- Kontrolujete kód na anti-patterny jako God Object, zneužívání Singletonu nebo zbytečné přeinženýrství
Principy SOLID v Drupalu
SOLID je zkratka pro pět návrhových principů, které dělají návrh softwaru srozumitelnějším, flexibilnějším a snáze udržovatelným.
1. Single-Responsibility Principle (SRP) — princip jedné odpovědnosti
Definice: „Nikdy by neměl existovat více než jeden důvod, proč třídu měnit" — každá třída by měla mít jen jednu ústřední odpovědnost.
Přínosy v Drupalu:
- Snazší testování: každou službu lze testovat nezávisle
- Lepší organizace modulu: jasné oddělení zodpovědností
- Znovupoužitelnost: služby lze využít v různých kontextech
Příklad:
// ❌ 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) — princip otevřenosti/uzavřenosti
Definice: „Softwarové entity by měly být otevřené pro rozšíření, ale uzavřené pro modifikaci"
Přínosy:
- Rozšiřitelnost bez zásahu do stávajícího kódu
- Nižší riziko zanesení chyb
- Lepší udržovatelnost
Příklad v Drupalu:
// 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) — princip substituce
Definice: „Objekty nadtřídy by mělo být možné nahradit objekty jejích podtříd, aniž by se rozbila funkčnost aplikace"
Klíčové požadavky:
- Předpodmínky (preconditions) nelze zpřísňovat
- Postpodmínky (postconditions) nelze oslabovat
- Invarianty musí zůstat zachovány
- Signatury metod musí odpovídat
Příklad:
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) — princip oddělení rozhraní
Definice: „Klienti by neměli být nuceni záviset na rozhraních, která nepoužívají"
Přínosy:
- Menší, zaměřená rozhraní
- Nižší provázanost (coupling)
- Snazší testování a údržba
Příklad:
// ❌ 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) — princip inverze závislostí
Definice: „Závisejte na abstrakcích, ne na konkrétních implementacích"
Přínosy:
- Volná provázanost mezi třídami
- Snazší testování díky dependency injection
- Vyšší flexibilita implementace
Příklad:
interface OrderStorageInterface {
public function saveOrder($orderData);
}
class OrderService {
public function __construct(OrderStorageInterface $storage) {
$this->storage = $storage;
}
public function processOrder($orderData) {
$this->storage->saveOrder($orderData);
}
}
Tvořivé vzory (Creational Patterns)
Factory Method Pattern
Účel: Definovat rozhraní pro vytváření objektů, ale nechat na podtřídách, kterou třídu instanciují.
Kdy použít:
- Když volající nemůže dopředu vědět, jaké typy objektů má vytvořit
- Když máte mnoho objektů společného typu
- Když chcete centralizovat logiku vytváření objektů
Příklad:
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
Účel: Oddělit konstrukci složitého objektu od jeho reprezentace.
Kdy použít:
- Když algoritmus vytváření složitého objektu má být nezávislý na jeho jednotlivých částech
- Když proces konstrukce musí umožňovat různé reprezentace výsledku
Příklad:
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
Účel: Zajistit, že třída má pouze jednu instanci, a poskytnout k ní globální přístupový bod.
Kdy použít:
- Když musí existovat právě jedna instance třídy
- Když má být tato instance dostupná globálně
Příklad:
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;
}
}
Strukturální vzory (Structural Patterns)
Adapter Pattern
Účel: Převést rozhraní třídy na jiné rozhraní, které klienti očekávají.
Kdy použít:
- Když chcete použít existující třídu s nekompatibilním rozhraním
- Když potřebujete vytvořit znovupoužitelnou třídu spolupracující s nesouvisejícími třídami
Příklad:
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
Účel: Skládat objekty do stromových struktur reprezentujících hierarchie typu část-celek.
Kdy použít:
- Když chcete reprezentovat hierarchie typu část-celek
- Když klienti mají s jednotlivými objekty i jejich kompozicemi zacházet jednotně
Příklad:
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
Účel: Definovat vztah typu jeden-k-mnoha mezi objekty, kdy při změně stavu jednoho objektu jsou informováni všichni jeho závislí odběratelé.
Kdy použít:
- Když změna jednoho objektu vyžaduje změnu dalších objektů
- Když má objekt umět upozornit ostatní objekty, aniž by je znal
Příklad:
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');
}
}
Behaviorální vzory (Behavioral Patterns)
Strategy Pattern
Účel: Definovat rodinu algoritmů, zapouzdřit každý z nich a učinit je vzájemně zaměnitelnými.
Kdy použít:
- Když existuje více způsobů, jak provést danou operaci
- Když potřebujete zvolit algoritmus až za běhu
Příklad:
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
Účel: Umožnit objektu měnit své chování při změně vnitřního stavu.
Kdy použít:
- Když chování objektu závisí na jeho stavu
- Když operace obsahují rozsáhlé, víceúrovňové podmínkové bloky
Příklad:
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-specifické vzory
Plugin Pattern
Účel: Umožnit modulům poskytovat rozšiřitelnou funkcionalitu prostřednictvím pluginů.
Příklad:
/**
* @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
Účel: Poskytovat centralizované služby, které lze injektovat do dalších tříd.
Příklad (services.yml):
services:
mymodule.custom_service:
class: Drupal\mymodule\Service\CustomService
arguments: ['@database', '@current_user']
Hook Pattern
Účel: Umožnit modulům měnit nebo rozšiřovat chování Drupalu.
Příklad:
function mymodule_node_presave(NodeInterface $node) {
if ($node->getType() == 'article') {
$node->setTitle(strtoupper($node->getTitle()));
}
}
Doporučené postupy
- Vyberte správný vzor: Nevnucujte vzor tam, kde stačí jednoduché řešení
- Udržujte to jednoduché: Vzory mají řešit problémy, ne vytvářet komplexitu
- Testujte implementace: Ověřte, že vzor ve vašem kontextu skutečně funguje
- Dokumentujte použití: Ať je jasné, které vzory používáte a proč
- Refaktorujte podle potřeby: Buďte ochotni vzor změnit, když se změní požadavky
Časté anti-patterny, kterým se vyhnout
- God Object: Třídy, které toho vědí nebo dělají příliš mnoho
- Zneužívání Singletonu: Používání singletonů na vše
- Těsná provázanost (Tight Coupling): Třídy příliš závislé jedna na druhé
- Zneužívání dědičnosti: Hluboké hierarchie dědičnosti
- Přeinženýrství (Over-engineering): Používání vzorů tam, kde nejsou potřeba