Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduction to Design Patterns with PHP

Hugo Hamon
November 19, 2013

Introduction to Design Patterns with PHP

Observer, decorator, factory method, composite… All these words refer to design patterns. Design patterns are general reusable solutions for solving common redundant problems in software architecture. Getting inspiration on some well known design patterns and implementing them in PHP will make your application code easier to understand, maintain and evolve. Based on practical code samples, this talk will focus on the most useful patterns in order to help you design better PHP applications.

Hugo Hamon

November 19, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Overview of OOP in PHP Objects and classes Abstract and

    final classes Interfaces Abstract & final Magic methods Garbage collector Object cloning Single inheritance SPL and more… Encapsulation Overloading (a bit...) Classes autoloading Type hinting Reflection Namespaces Traits
  2. SOLID Principle Single Responsibility Principle Open Close Principle Liskov Substitution

    Principle Interface Segregation Principle Dependency Injection Principle
  3. Single Responsability (SRP) « A class should have one, and

    only one, reason to change. » Keep your classes small and focused on small responsabilities.
  4. class CsvDataImporter { public function import($file) { $records = array();

    if (false !== $handle = fopen($file, 'r')) { while ($record = fgetcsv($handle)) { $records[] = $record; } } fclose($handle); try { $this->db->beginTransaction(); foreach ($records as $record) { $stmt = $this->db->prepare('INSERT INTO ...'); $stmt->execute($record); } $this->db->commit(); } catch (PDOException $e) { $this->db->rollback(); throw $e; } } }
  5. class DataImporter { private $loader; private $table; function __construct(FileLoader $loader,

    TableGateway $table) { $this->loader = $loader; $this->table = $table; } public function import($file) { $records = $this->loader->load($file); return $this->table->insert($records); } }
  6. Open Close (OCP) Software entities should be open to extension

    and close to modifications. « You should be able to extend a classe’s behavior, without modifying it. »
  7. Open Close $loader = new CsvFileLoader(); $loader = new XmlFileLoader();

    $loader = new JsonFileLoader(); $importer = new DataImporter($loader, ...); $importer->import('foo.csv|xml|json'); Using a different file loader is now very easy without changing the importer class.
  8. Liskov Substitution A dependency of type T should be replaced

    by another subtype S of T without breaking the code. « Derived classes must be substitutable for their base classes. »
  9. Liskov Substitution class TextFileLoader implements FileLoader { public function load($file)

    { // ... return [ ... ]; } } class CsvFileLoader extends TextFileLoader { public function load($file) { // ... return [ ... ]; } }
  10. Interface Segregation (ISP) Interface segregation split larger interfaces into smaller

    ones so that clients will only know about the methods they need to work with. « Make fine grained interfaces that are client specific. »
  11. Interface Segregation interface FileLoader { public function load($file); public function

    supports($type); } interface TableGateway { public function insert(array $records); }
  12. Interface Segregation class DataImporter { public function __construct(FileLoader $loader, TableGateway

    $table) { // ... } public function import($file) { $ext = pathinfo($file, PATHINFO_EXTENSION); if (!$this->loader->supports($ext)) { throw new UnsupportedDataFile(); } $records = $this->loader->load($file); return $this->table->insert($records); } }
  13. Dependency Injection (DIP) D.I is where components are given their

    dependencies through their constructors, methods, or directly into fields. « Depend on abstractions, not on concretions. »
  14. DI – The Wrong Way class DataImporter { private $loader;

    private $table; public function __construct() { $this->loader = new CsvFileLoader(); $this->table = new TableGateway(); } }
  15. DI – The Right Way class DataImporter { private $loader;

    private $table; function __construct(FileLoader $loader, TableGateway $table) { $this->loader = $loader; $this->table = $table; } }
  16. DI – Injecting dependencies $importer = new DataImporter( new CsvFileLoader(),

    new TableGateway('data', new PDO(…)) ); $importer->import('foo1.csv'); $importer->import('foo2.csv'); $importer->import('foo3.csv');
  17. Definition In software design, a design pattern is an abstract

    generic solution to solve a particular redundant problem.
  18. Design patterns families Abstract Factory Builder Factory Method Lazy Initialization

    Prototype Singleton Adapter Bridge Composite Decorator Facade Flyweight Proxy Chain of Responsability Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor Creational Structural Behavioral
  19. The Context A web store wants to sell hard and

    digital products. Shipment fees are calculated based on the product caracteristics (volume, mass, type…) Products are shipped to customers.
  20. abstract class Product { protected $name; protected $price; public function

    __construct($name, Money $price) { $this->name = $name; $this->price = $price; } abstract public function getMass(); abstract public function getVolume(); // ... a bunch of getters }
  21. class HardProduct extends Product { // ... private $mass; private

    $volume; public function setPhysics(Mass $mass, Volume $volume) { $this->mass = $mass; $this->volume = $volume; } public function getMass() { return $this->mass; } public function getVolume() { return $this->volume; } }
  22. class DigitalProduct extends Product { public function getMass() { return

    new Mass(0); } public function getVolume() { return new Volume(0); } }
  23. class Bundle extends Product { protected $products; public function __construct($name)

    { $this->name = $name; } public function setPrice(Money $price) { $this->price = $price; } public function add(ProductInterface $product) { $this->products[] = $product; } }
  24. class Bundle extends Product { public function getMass() { $mass

    = new Mass(0); foreach ($this->products as $product) { $mass = $mass->add($product->getMass()); } return $mass; } public function getVolume() { $volume = new Volume(0); foreach ($this->products as $product) { $volume = $volume->add($product->getVolume()); } return $volume; } }
  25. class Bundle extends Product { public function getPrice() { if

    ($this->price) { return $this->price; } $price = $this->products[0]->getPrice(); for ($i = 1; $i < count($this->products); $i++) { $product = $this->products[$i]; $price = $price->add($product->getPrice()); } return $price; } }
  26. $b1 = new Bundle('C'); $b1->setPrice(new Money(39)); $b1->addProduct(new HardProduct('A', new Money(29)));

    $b1->addProduct(new DigitalProduct('B', new Money(20))); echo $b1->getPrice(); // 39 EUR $b2 = new Bundle('D'); $b2->addProduct(new HardProduct('C', new Money(12))); echo $b2->getPrice(); // 12 EUR Usage: creating products
  27. $b1 = new Bundle('C'); $b1->setPrice(new Money(39)); $b1->addProduct(new HardProduct('A', new Money(29)));

    $b1->addProduct(new DigitalProduct('B', new Money(20))); $b2 = new Bundle('D'); $b2->addProduct(new HardProduct('C', new Money(12))); $b3 = new Bundle('E'); $b3->addProduct($b1); $b3->addProduct($b2); echo $b3->getPrice(); // 39 + 12 = 51 EUR Usage: embedding bundles
  28. Common implementations 1.  Adding some caching capability to a web

    service client to avoid network calls if not needed. 2.  Adding some logging features to an object in a dev environment. For instance, a database connection.
  29. Concrete Example We want to offer discounts on orders without

    breaking existing domain model. Discount strategies may vary depending on the context.
  30. class Order implements OrderInterface { private $products = array(); public

    function addProduct(Product $product) { $this->products[] = $product; } public function getAmount() { $total = new Money(0); foreach ($this->products as $product) { $total = $total->add($product->getPrice()); } return $total; } }
  31. abstract class OrderDecorator implements OrderInterface { protected $order; public function

    __construct(OrderInterface $order) { $this->order = $order; } public function addProduct(Product $product) { return $this->order->addProduct($product); } }
  32. class BasicValueDiscount extends OrderDecorator { private $discount; function __construct(OrderInterface $order,

    Money $discount) { parent::__construct($order); $this->discount = $discount; } function getAmount() { $amount = $this->order->getAmount(); return $amount->subtract($this->discount); } }
  33. $order = new Order(...); $order->addProduct(new DigitalProduct('A', new Money(15))); $order->addProduct(new DigitalProduct('B',

    new Money(10))); // Substract 5 euros to the total $order1 = new BasicValueDiscount($order, new Money(5)); $order1->getAmount(); // 20 EUR // Substract 50% of the total $order2 = new BasicRateDiscount($order, 50); $order2->getAmount(); // 12.50 EUR // Combine discounts together $order3 = new BasicRateDiscount( new BasicValueDiscount($order, new Money(5)), 50 ); $order3->getAmount(); // 10 EUR Usage
  34. Other discount strategies… + Minimum purchase amount required + Minimum

    ordered items required + Loyal clients only + Special first order discount + …
  35. Pros and cons + Easy way to extend an object

    capabilities + No need to change the underlying code - Object construction becomes more complex - Difficulty to test the concrete object type
  36. The strategy pattern encapsulates algorithms of the same nature into

    dedicated classes to make them interchangeable. Goal
  37. Common implementations 1.  Switching to another storage engine to store

    session data. 2.  Comparing and sorting elements of an array. 3.  Sending emails with SMTP, Sendmail or the mail() function.
  38. class Swift_Mailer { private $transport; public function __construct(Swift_Transport $transport) {

    $this->transport = $transport; } public function send(Swift_Message $message) { // do stuff… $this->transport->send($message); } } Swiftmailer example
  39. class Swift_MailTransport extends Swift_Transport { public function send(Swift_Message $message) {

    // use php mail() function to send a message } } class Swift_SmtpTransport extends Swift_Transport { public function send(Swift_Message $message) { // use an SMTP connection to send a message } } class Swift_NullTransport extends Swift_Transport { public function send(Swift_Message $message) { // fake email sending } }
  40. Concrete example We want to calculate the order shipment fees

    depending on the chosen delivery strategy. The customer can choose between 3 delivery strategies.
  41. Concrete example (wrong!!!) class OrderService { function priceShippingCost(OrderInterface $order, $type)

    { switch ($type) { case 'warehouse-pickup': return new Money(0); case 'regular': return new Money(7); case 'express': return new Money(10); } } }
  42. Encapsulating algorithms interface ShippingStrategy { function calculateShippingCost(OrderInterface $order); } class

    WarehousePickup implements ShippingStrategy { function priceShippingCost(OrderInterface $order) { return new Money(0); } }
  43. Encapsulating algorithms class RegularShipping implements ShippingStrategy { function priceShippingCost(OrderInterface $order)

    { if ($order->getAmount() > 100) { return new Money(0); } if (count($order->getProduct()) > 3) { return new Money(12); } return new Money(7); } }
  44. Encapsulating algorithms class ExpressShipping implements ShippingStrategy { function priceShippingCost(OrderInterface $order)

    { if ($order->getWeight() < 5) { return new Money(6); } if ($order->getWeight() >= 5 && $order->getWeight() < 10) { return new Money(9); } return new Money(15); } }
  45. class OrderService { public function priceShippingCost( OrderInterface $order, ShippingStrategy $shipping

    ) { // ... do stuff $fare = $shipping->priceShippingCost($order); $order->getAmount()->add($fare); // ... do stuff } } Strategy can be given as an argument of the client’s method.
  46. class OrderService { private $shippingStrategy; public function __construct(ShippingStrategy $shipping) {

    $this->shippingStrategy = $shipping; } public function priceShippingCost(OrderInterface $order, $delivery) { // ... do stuff $fare = $this->shippingStrategy->priceShippingCost($order); $order->getAmount()->add($fare); // ... do stuff } } Or it can be injected in the constructor method of the client.
  47. Goal Define an interface for creating an object, but let

    subclasses decide which class to instantiate.
  48. Concrete example We want to encapsulate the way we create

    hard products, digital products and bundles. The goal is to keep instantiation processes in one single place.
  49. abstract class ProductFactory { public function __construct(ReferenceGenerator $generator) { $this->refGenerator

    = $generator; } abstract protected function createProduct($name, $price, $mass = null, $volume = null); public function newProduct($name, $price, $mass = null, $volume = null) { $price = new Price($price, new Currency('EUR')); $product = $this->createProduct($name, $price, $mass, $volume); $product->setReference($this->refGenerator->generate($product)); return $product; } } Usage
  50. class HardProductFactory extends ProductFactory { protected function createProduct( $name, Price

    $price, Mass $mass = null, Volume $volume = null ) { $product = new HardProduct($name, $price); $product->setPhysics($mass, $volume); return $product; } }
  51. class DigitalProductFactory extends ProductFactory { protected function createProduct( $name, Price

    $price, Mass $mass = null, Volume $volume = null ) { return new DigitalProduct($name, $price); } }
  52. class BundleFactory extends ProductFactory { protected function createProduct( $name, Price

    $price, Mass $mass = null, Volume $volume = null ) { return new Bundle($name, $price); } }
  53. $generator = new ReferenceGenerator(); $factory = new HardProductFactory($generator); $product =

    $factory->newProduct('A', 50, 4, 10); $factory = new DigitalProductFactory($generator); $product = $factory->newProduct('B', 25); $factory = new BundleFactory($generator); $bundle = $factory->newProduct('C', 99); Usage
  54. Goal A subject, the observable, emits a signal to a

    list of modules known as observers.
  55. class Customer { public function pay(Order $order, Money $amount) {

    $order->setPaid(); if ($this->logger) { $this->logger->log('New order...'); } $mail = new Email(); $mail->recipient = $this->customer->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  56. interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface {

    function attach(ObserverInterface $observer); function notifyObservers(); }
  57. class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface

    $subject) { $order = $subject->getLastOrder(); $reference = $order->getReference(); $this->logger->log('New order #'. $reference); } }
  58. class CustomerNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface

    $subject) { $mail = new Email(); $mail->recipient = $subject->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); } }
  59. class SalesNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface

    $subject) { $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  60. class Customer implements ObservableInterface { // ... private $observers; public

    function attach(ObserverInterface $observer) { $this->observers[] = $observer; } public function notifyObservers() { foreach ($this->observers as $observer) { $observer->notify($this); } } }