$30 off During Our Annual Pro Sale. View Details »

Implementing Design Patterns with PHP

Implementing Design Patterns with PHP

SOLID Principle
Creational Patterns: Factory Method
Structural Patterns: Composite, Decorator
Behavioral Patterns: Visitor

Hugo Hamon

March 02, 2017
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Implementing Design Patterns with PHP
    AFSY 2017 – Bordeaux – Hugo Hamon
    https://www.flickr.com/photos/64667396@N00/1668455127/sizes/o/

    View Slide

  2. Hugo Hamon
    Software Architect
    SensioLabs
    Book author
    Conferences speaker
    Symfony contributor
    Bengal cat fan
    @hhamon

    View Slide

  3. Introduction

    View Slide

  4. What are design patterns?
    In software design, a design
    pattern is an abstract generic
    solution to solve a particular
    redundant problem.

    View Slide

  5. Design Patterns Classification
    23 « Gang of Four » Design Patterns
    ¤  Creational
    ¤  Structural
    ¤  Behavioral

    View Slide

  6. Benefits of Design Patterns
    ¤  Communication & vocabulary
    ¤  Testability
    ¤  Maintainance
    ¤  Extensibility
    ¤  Loose coupling

    View Slide

  7. Downsides of Design Patterns
    ¤  Hard to teach and learn
    ¤  Hard to know when to apply
    ¤  Require good knowledge of OOP
    ¤  Not always the holly grail!

    View Slide

  8. Design Patterns encourage SOLID code
    SRP / Single Responsability Principle
    OCP / Open / Closed Principle
    LSP / Liskov Substitution Principle
    ISP / Interface Segregation Principle
    DIP / Dependency Inversion Principle

    View Slide

  9. Creational Patterns
    Creational design patterns
    encapsulate and isolate the
    algorithms to create and initialize
    objects.
    Abstract Factory – Builder – Factory Method
    Lazy Initialization – Prototype – Singleton

    View Slide

  10. Factory Method
    Define an interface for creating an
    object, but let subclasses decide which
    class to instantiate. The Factory method
    lets a class defer instantiation it uses
    to subclasses.
    - GoF

    View Slide

  11. The Need For a Factory
    The Factory object encapsulates the process for creating,
    assembling and initializing objects. It hides this whole
    process from the client code that just needs to get a new
    made object.
    Having a Factory also allows to change the process for
    creating objects at any point in time without impacting
    the client code that still relies on the same interface.

    View Slide

  12. The Static Method Approach
    class MediaFactory
    {
    public static function createMedia($path)
    {
    $format = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if ('pdf' === $format) {
    return new PdfFile($path);
    }
    if (in_array($format, [ 'jpg', 'png', 'gif' ])) {
    return new ImageFile($path);
    }
    if ('txt' === $format) {
    return new TextFile($path);
    }
    throw new \UnexpectedValueException('Unexpected format '. $format);
    }
    }

    View Slide

  13. The Static Method in Action
    class DocumentRepository
    {
    private $filesystem;
    private $imageManipulator;
    // ...
    public function move($path, $target)
    {
    $file = MediaFactory::createMedia($path);
    if ($file instanceof ImageFile) {
    $file = $this->imageManipulator->resize($file, 120, 120);
    }
    $this->filesystem->move($file->getRealPath(), $target);
    }
    }

    View Slide

  14. Pros & Cons of Static Factories
    Tight coupling between the factory and the client,
      Client code is more difficult to unit test,
    Unable to switch to another factory,
    Difficulty to rely on dependencies in the factory.
    Very simple,
    Very pragmatic,
    Easy to understand.

    View Slide

  15. The Simple Factory Approach
    class MediaFactory implements MediaFactoryInterface
    {
    public function createMedia($path)
    {
    $format = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if ('pdf' === $format) {
    return new PdfFile($path);
    }
    if (in_array($format, [ 'jpg', 'png', 'gif' ])) {
    return new ImageFile($path);
    }
    if ('txt' === $format) {
    return new TextFile($path);
    }
    throw new \UnexpectedValueException('Unexpected format '. $format);
    }
    }

    View Slide

  16. The Simple Method in Action
    class DocumentRepository
    {
    private $mediaFactory;
    // ...
    public function __construct(MediaFactoryInterface $mediaFactory, ...)
    {
    $this->mediaFactory = $mediaFactory;
    // ...
    }
    public function move($path, $target)
    {
    $file = $this->mediaFactory->createMedia($path);
    // ...
    }
    }

    View Slide

  17. Pros & Cons of Simple Factories
    It’s still not a real factory method object.
      The factory produces multiple kinds of objects.
      Simple & pragmatic approach.
    Loose coupling between factory and client code.
    Leverages the dependency injection principle.
    Easy to unit test the client code.
    Easy to replace the actual factory by another one.

    View Slide

  18. The Real Factory Method Design Pattern

    View Slide

  19. Implementing Real Factory Method
    The goal is to build a flexible system able to analyze and extract the
    metadata of any kind of media files like pictures, video clips or music
    clips.
    Depending on the file type to analyze, a concrete factory object must
    be chosen. It will be responsible for creating and initializing a concrete
    type metadata class. Every concrete metadata object must inherit the
    attributes of an abstract parent Metadata class.

    View Slide

  20. View Slide

  21. Concrete Factories in Action – 1/2
    $factory = new ImageMetadataFactory(new ImageAnalyzer());
    /** @var ImageMetadata $metadata */
    $metadata = $factory->loadMetadata('/path/to/image.png');
    echo 'Path: ', $metadata->getRealPath() ,"\n";
    echo 'Date: ', $metadata->getCreatedAt() ,"\n";
    echo 'Size: ', $metadata->getSize() ,"\n";
    echo 'Width: ', $metadata->getWidth() ," px\n";
    echo 'Height: ', $metadata->getHeight() ," px\n";
    echo 'Orientation: ', $metadata->getOrientation() ,"\n";

    View Slide

  22. Concrete Factories in Action – 2/2
    $factory = new MovieMetadataFactory(new VideoAnalyzer());
    /** @var MovieMetadata $metadata */
    $metadata = $factory->loadMetadata('/path/to/movie.mp4');
    echo 'Path: ', $metadata->getRealPath() ,"\n";
    echo 'Date: ', $metadata->getCreatedAt() ,"\n";
    echo 'Size: ', $metadata->getSize() ,"\n";
    echo 'Resolution X: ', $metadata->getXResolution() ,' px ', "\n";
    echo 'Resolution Y: ', $metadata->getYResolution() ,' px ', "\n";
    echo 'Duration: ', $metadata->getDuration() ," seconds\n";
    echo 'Frame rate: ', $metadata->getFrameRate() ," fps\n";
    echo 'Frames: ', $metadata->getFrameCount() ," frames\n";

    View Slide

  23. Designing the MediaMetadata Class?
    abstract class MediaMetadata
    {
    private $realPath;
    private $size;
    private $createdAt;
    public function initialize(\SplFileInfo $file)
    {
    $this->size = $file->getSize();
    $this->realPath = $file->getRealPath();
    $this->createdAt = $file->getMTime();
    }
    // … plus one getter for each private property
    }

    View Slide

  24. Designing the MediaMetadataFactory Class?
    abstract class MediaMetadataFactory implements MediaMetadataFactoryInterface
    {
    private $analyzer;
    public function __construct(MediaAnalyzerInterface $analyzer)
    {
    $this->analyzer = $analyzer;
    }
    abstract protected function createMetadata(\SplFileInfo $file);
    protected function analyze(\SplFileInfo $file)
    {
    return $this->analyzer->analyze($file);
    }
    }

    View Slide

  25. Designing the MediaMetadataFactory Class?
    abstract class MediaMetadataFactory implements MediaMetadataFactoryInterface
    {
    // ...
    /** @return MediaMetada */
    public function loadMetadata($path)
    {
    if (!is_readable($path)) {
    throw new MediaNotFoundException(sprintf('%s not readable.', $path));
    }
    $file = new \SplFileInfo($path);
    $metadata = $this->createMetadata($file);
    $metadata->initialize($file);
    return $metadata;
    }
    }

    View Slide

  26. Analyzing Image Files
    Each ImageMetadata instance has a real path, a size, a
    creation date but also a set of dimensions (width & height)
    and an orientation (portrait, landscape or square).
    The ImageMetadataFactory class is responsible for
    producing ImageMetadata objects. To analyze a picture,
    the factory uses an ImageAnalyzer instance.

    View Slide

  27. The Concrete ImageMetadata Class
    class ImageMetadata extends MediaMetadata
    {
    const SQUARE = 'square';
    const PORTRAIT = 'portrait';
    const LANDSCAPE = 'landscape';
    private $width;
    private $height;
    public function __construct($width, $height)
    {
    $this->width = (int) $width;
    $this->height = (int) $height;
    }
    // … plus getter methods
    }

    View Slide

  28. Reading Image Information with an Analyzer
    class ImageAnalyzer implements MediaAnalyzerInterface
    {
    public function analyze(\SplFileInfo $file)
    {
    $path = $file->getRealPath();
    if (!$path || !$metadata = @getimagesize($path)) {
    throw new AnalysisFailedException(sprintf(
    'Unable to extract image metadata for path %s.',
    $path
    ));
    }
    return [ 'width' => $metadata[0], 'height' => $metadata[1] ];
    }
    }

    View Slide

  29. Implementing the ImageMetadataFactory Class
    class ImageMetadataFactory extends MediaMetadataFactory
    {
    /**
    * Creates the specific ImageMetadata object.
    *
    * @param \SplFileInfo $file
    * @return ImageMetadata
    */
    protected function createMetadata(\SplFileInfo $file)
    {
    $infos = $this->analyze($file);
    return new ImageMetadata($infos['width'], $infos['height']);
    }
    }

    View Slide

  30. Using the ImageMetadataFactory Object
    $factory = new ImageMetadataFactory(new ImageAnalyzer());
    $metadata = $factory->loadMetadata('/path/to/image.png');
    echo 'Path: ', $metadata->getRealPath() ,"\n";
    echo 'Date: ', $metadata->getCreatedAt() ,"\n";
    echo 'Size: ', $metadata->getSize() ,"\n";
    echo 'Width: ', $metadata->getWidth() ," px\n";
    echo 'Height: ', $metadata->getHeight() ," px\n";
    echo 'Orientation: ', $metadata->getOrientation() ,"\n";

    View Slide

  31. Pros & Cons of Factories
      Harder to implement,
    Involves lots of classes and interfaces,
      Client still doesn’t know the exact type of products.
    Managing new file types means creating new factories,
    Easy to swap a factory with another one,
    Each factory only produces one concrete type of object,
    Objects instanciations are centralized,
    Provides lots of flexibility to the code.

    View Slide

  32. Structural Patterns
    Structural patterns organize classes
    and help separating the
    implementations from their
    interfaces.
    Adapter – Bridge – Composite – Decorator
    Facade – Flyweight – Proxy

    View Slide

  33. Composite
    Processing single objects and collections of
    objects uniformly. Every single object and
    collections share the same unified interface.
    Composite design pattern enables to combine
    objects and perform recursive operations very
    easily.

    View Slide

  34. The Need for Composite Objects
      File explorer
    Organization chart
    Family tree
      Content management (nested pages)
      XML file parsing
    Nested web forms
      Navigation bar (items including subitems)
      …

    View Slide

  35. The Composite Design Pattern

    View Slide

  36. Composite Design Pattern Usage
    $leaf = new ConcreteLeaf();
    $leaf->operation();
    $simpleComposite = new ConcreteComposite();
    $simpleComposite->add(new ConcreteLeaf());
    $simpleComposite->add(new ConcreteLeaf());
    $simpleComposite->operation();
    $superComposite = new ConcreteComposite();
    $superComposite->add(new ConcreteLeaf());
    $superComposite->add(new ConcreteLeaf());
    $superComposite->add($simpleComposite);
    $simpleComposite->operation();

    View Slide

  37. Implementing the Composite Pattern
    The goal is to build a flexible ecommerce system able to sell
    single articles (physical or digital) and combos of articles for a
    cheaper price.
    Each article must have a unit price and a mass. The mass of a
    combo is the sum of the masses of all articles of the combo. The
    unit price of the combo can be a fixed price or the sum of all
    unit prices of the articles in the combo.

    View Slide

  38. UML Diagram Representation

    View Slide

  39. Designing the Base Product Class
    abstract class Product
    {
    protected $name;
    protected $price;
    protected $mass;
    public function __construct($name, Money $price, Mass $mass)
    {
    $this->name = $name;
    $this->price = $price;
    $this->mass = $mass;
    }
    // + getter methods for the attributes…
    }

    View Slide

  40. Designing the Concrete Product Classes
    class HardProduct extends Product
    {
    }
    class DigitalProduct extends Product
    {
    public function __construct($name, Money $price)
    {
    parent::__construct($name, $price, new Mass(0));
    }
    }

    View Slide

  41. Using the Concrete Product Classes
    $paperBook = new HardProduct(
    'PHP Design Patterns',
    Money::fromString('EUR 49.00'),
    Mass::fromString('960.00 g')
    );
    $digitalBook = new DigitalProduct(
    'PHP Design Patterns',
    Money::fromString('EUR 22.00')
    );

    View Slide

  42. Designing the Combo Class
    class Combo extends Product
    {
    private $products;
    function __construct($name, array $products, Money $price = null)
    {
    $this->setProducts($products);
    $price = $price ?: $this->getTotalPrice();
    parent::__construct($name, $price, $this->getTotalMass());
    }
    // ...
    private function setProducts(array $products)
    {
    $this->products = $products;
    }
    }

    View Slide

  43. Calculate the Total Price of the Combo
    class Combo extends Product
    {
    // ...
    private function getTotalPrice()
    {
    $total = $this->getPriceAt(0);
    for ($i = 1; $i < count($this->products); $i++) {
    $total = $total->add($this->getPriceAt($i));
    }
    return $total;
    }
    private function getPriceAt($index)
    {
    return $this->products[$index]->getPrice();
    }
    }

    View Slide

  44. Calculate the Total Mass of the Combo
    class Combo extends Product
    {
    // ...
    private function getTotalMass()
    {
    $total = $this->getMassAt(0);
    for ($i = 1; $i < count($this->products); $i++) {
    $total = $total->add($this->getMassAt($i));
    }
    return $total;
    }
    private function getMassAt($index)
    {
    return $this->products[$index]->getMass();
    }
    }

    View Slide

  45. Creating a Combo with a Fixed Price
    $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera & Bag', $products, new EUR(83900));
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Mass: ', $combo->getMass()->getValue() ," g\n";
    echo 'Price: ', $combo->getPrice()->getConvertedAmount() ," €\n";
    Name: Digital Camera & Bag
    Mass: 1117 g
    Price: 839.00 €

    View Slide

  46. Creating a Combo with a Dynamic Price
    $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera & Bag', $products);
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Mass: ', $combo->getMass()->getValue() ,"\n";
    echo 'Price: ', $combo->getPrice()->getConvertedAmount() ,"\n";
    Name: Digital Camera & Bag
    Mass: 1117 g
    Price: 907.00 €

    View Slide

  47. Creating a Supper Dupper Combo
    $products = [
    new HardProduct('Digital Camera', new EUR(78900), new Mass(855)),
    new HardProduct('Camera Bag', new EUR(3900), new Mass(220)),
    new HardProduct('Memory Card 128 Gb', new EUR(7900), new Mass(42)),
    ];
    $combo = new Combo('Digital Camera Combo Pack + Tripod', [
    new HardProduct('Lightweight Tripod', new EUR(2690), new Mass(570)),
    new Combo('Digital Camera & Bag', $products, new EUR(83900)),
    ]);
    echo 'Name: ', $combo->getName() ,"\n";
    echo 'Weight: ', $combo->getWeight()->getValue() ,"\n";
    echo 'Price: ', $combo->getPrice()->getConvertedAmount() ,"\n";
    Name: Digital Camera Combo Pack + Tripod
    Mass: 1687 g
    Price: 865.90 €

    View Slide

  48. Decorator
    The Decorator design pattern allows to
    dynamically add new responsabilities to
    an object without changing its class.
    This design pattern encourages objects
    composition over class inheritance.

    View Slide

  49. The Decorator Design Pattern

    View Slide

  50. The Decorator Design Pattern Usage
    $instance = new OtherConcreteDecorator(
    new SomeConcreteDecorator(
    new ConcreteDecorator(
    new DecoratedComponent()
    )
    )
    );
    $instance->operation();

    View Slide

  51. Implementing the Decorator Pattern
    The goal is to build a flexible coupons system in order to
    discount the total price of an order upon certain conditions.
    Each discount coupon can be either a rate discount (-15%) or a
    discount value (-10€).
    Each coupon can also have zero or multiple restrictions to
    constraint their eligibility. The system must allow to create
    coupons with any complex combination for restrictions.

    View Slide

  52. Coupon Restrictions Ideas
    Valid for a specific period of time,
      Minimum total amount required,
      Minimum quantity of ordered items required,
    Valid for a specific geographical area,
      Customer must own the loyalty membership card,
      Coupon is valid for Premium / VIP customers only,
    Valid for the customer’s very first order,
    Some products are not eligible for discounts,
    Valid only on some specific products areas (luxury, food…),
      etc.

    View Slide

  53. UML Diagram

    View Slide

  54. Designing the ValueCoupon Class
    class ValueCoupon implements CouponInterface
    {
    private $code;
    private $discount;
    public function __construct($code, Money $discount)
    {
    $this->code = $code;
    $this->discount = $discount;
    }
    public function getCode()
    {
    return $this->code;
    }
    public function applyDiscount(Order $order)
    {
    return $order->addDiscount($this->discount);
    }
    }

    View Slide

  55. Designing the RateCoupon Class
    class RateCoupon implements CouponInterface
    {
    private $code;
    private $rate;
    public function __construct($code, $rate)
    {
    $this->code = $code;
    $this->rate = $rate;
    }
    public function getCode()
    {
    return $this->code;
    }
    public function applyDiscount(Order $order)
    {
    $amount = $order->getTotalAmount();
    return $order->addDiscount($amount->multiply($this->rate));
    }
    }

    View Slide

  56. Designing the Abstract Decorator Class
    abstract class CouponDecorator implements CouponInterface
    {
    protected $coupon;
    public function __construct(CouponInterface $coupon)
    {
    $this->coupon = $coupon;
    }
    public function getCode()
    {
    return $this->coupon->getCode();
    }
    protected function createCouponException($message, \Exception $previous = null)
    {
    return new CouponException($message, 0, $previous);
    }
    }

    View Slide

  57. The LimitedLifetimeCoupon Class
    class LimitedLifetimeCoupon extends CouponDecorator
    {
    private $startAt;
    private $expiresAt;
    public function __construct(CouponInterface $coupon, $startAt, $expiresAt)
    {
    if (!$startAt instanceof \DateTime) {
    $startAt = new \DateTime($startAt);
    }
    if (!$expiresAt instanceof \DateTime) {
    $expiresAt = new \DateTime($expiresAt);
    }
    if ($startAt > $expiresAt) {
    throw new \InvalidArgumentException('...');
    }
    parent::__construct($coupon);
    $this->startAt = $startAt;
    $this->expiresAt = $expiresAt;
    }
    }

    View Slide

  58. class LimitedLifetimeCoupon extends CouponDecorator
    {
    public function applyDiscount(Order $order)
    {
    $now = new \DateTime('now');
    if ($this->startAt > $now) {
    throw $this->createCouponException(sprintf(
    'Coupon is usable from %s.',
    $this->startAt->format('Y-m-d H:i:s')
    ));
    }
    if ($now > $this->expiresAt) {
    throw $this->createCouponException(sprintf(
    'Coupon was valid until %s.',
    $this->expiresAt->format('Y-m-d H:i:s')
    ));
    }
    return $this->coupon->applyDiscount($order);
    }
    }

    View Slide

  59. Using the LimitedLifetimeCoupon Class
    // Coupon offers a discount value of 20 €.
    $value = Money::fromString('EUR 20.00');
    $coupon = new LimitedLifetimeCoupon(
    new ValueCoupon('3s2h7pd65s', $value),
    '2016-09-01',
    '2016-09-30'
    );
    // Coupon offers 25% off.
    $coupon = new LimitedLifetimeCoupon(
    new RateCoupon('76cqa6qr19', .25),
    '2016-09-01',
    '2016-09-30'
    );

    View Slide

  60. The MinimumPurchaseAmountCoupon Class
    class MinimumPurchaseAmountCoupon extends CouponDecorator
    {
    private $minimumAmount;
    function __construct(CouponInterface $coupon, Money $minAmount)
    {
    parent::__construct($coupon);
    $this->minimumAmount = $minAmount;
    }
    public function applyDiscount(Order $order)
    {
    $amount = $order->getTotalAmount();
    if ($amount->lessThan($this->minimumAmount)) {
    throw $this->createCouponException(...);
    }
    return $this->coupon->applyDiscount($order);
    }
    }

    View Slide

  61. Using the MinimumPurchaseAmountCoupon Class
    $minAmount = Money::fromString('EUR 300');
    // Get 20 € off if total amount is greater than 300 €
    $discount = Money::fromString('EUR 20');
    $coupon = new MinimumPurchaseAmountCoupon(
    new ValueCoupon('3s2h7pd65s', $discount),
    $minAmount
    );
    // Get 25% off if total amount is greater than 300 €
    $coupon = new MinimumPurchaseAmountCoupon(
    new RateCoupon('76cqa6qr19', .25),
    $minAmount
    );

    View Slide

  62. Combining Multiple Restrictions Together
    $totalAmount = Money::fromString('EUR 200.00');
    $minAmount = Money::fromString('EUR 170.00');
    $discount = Money::fromString('EUR 20');
    // Create the special coupon
    $coupon = new ValueCoupon('3s2h7pd65s', $discount);
    $coupon = new LimitedLifetimeCoupon($coupon, 'now', '+60 days');
    $coupon = new MinimumPurchaseAmountCoupon($coupon, $minAmount);
    $coupon = new CustomerFirstOrderCoupon($coupon);
    // Create the order instance
    $customer = new Customer('[email protected]');
    $order = new Order($customer, $totalAmount);
    // Apply discount coupon on the order
    $discountedAmount = $coupon->applyDiscount($order);

    View Slide

  63. Pros & Cons of Decorators
      The decorated object class remains unchanged,
    Leverage the SRP and OCP principles,
    Allow to support any complex decoration combinations,
    Ability to choose in which order decorations are applied.
      The variable doesn’t always contain the « real » object,
      All method calls must be forwarded,
      Not suitable for interfaces with lots of public methods,
    Doesn’t work for methods that returns the current instance.

    View Slide

  64. Behavioral Patterns
    Behavioral patterns simplify the
    communication between objects in
    order to increase flexibility and
    decoupling.
    Chain of Responsability – Command – Interpreter
    Iterator – Mediator – Memento – Observer – State
    Strategy – Template Method – Visitor

    View Slide

  65. Visitor
    The Visitor design pattern
    represents an operation to be
    performed on elements of an object
    structure. Visitor lets you define
    a new operation without changing
    the classes of the elements on
    which it operates.

    View Slide

  66. UML Diagram

    View Slide

  67. The Main API
    class ConcreteElement implement ElementInterface
    {
    public function accept(VisitorInterface $visitor)
    {
    $visitor->visit($this);
    }
    }
    class ConcreteVisitor implement VisitorInterface
    {
    public function visit(ElementInterface $element)
    {
    // do something with the element
    }
    }

    View Slide

  68. Visitor Design Pattern Usage
    $element1 = new ConcreteElement();
    $element1->accept(new ConcreteVisitorA());
    $element1->accept(new ConcreteVisitorB());
    $element1->accept(new ConcreteVisitorC());
    $element2 = new OtherConcreteElement();
    $element2->accept(new ConcreteVisitorD());
    $element2->accept(new ConcreteVisitorC());
    The accept() method of each concrete element receives a concrete instance of the
    Visitor type and invokes the its visit() method on it. The concrete element injects itself
    into the visit() method list of arguments.

    View Slide

  69. Implementing the Visitor Pattern
    Consider a ShoppingCart object which contains a list
    of ShoppingCartItem instances. Each of these
    instances holds a reference to a Product reference
    object and the ordered quantity.
    During checkout, we want to perform several
    operations on the shopping cart like computing the
    total price, sorting all products per categories or even
    issueing the receipt.

    View Slide

  70. Designing a Shopping Cart System
    $cart = new ShoppingCart();
    $cart->addItem(0.65, new Tomatoes('Vegetables', new EUR(189)));
    $cart->addItem(1.38, new Salmon('Seafood', new EUR(2250)));
    $cart->addItem(2, new ToothpasteTube('Wellness', new EUR(115)));
    $cart->addItem(0.89, new Apples('Vegetables', new EUR(128)));
    $cart->addItem(3, new Shampoo('Wellness', new EUR(123)));
    $cart->addItem(0.47, new Tuna('Seafood', new EUR(1980)));
    $cart->addItem(0.26, new GoatCheese('Creamery', new EUR(1723)));
    Each product reference object has a category and a
    price. The price can be either a unit price (for a
    shampoo for instance) or a price per kilogram (for
    vegetables or seafood).

    View Slide

  71. Performing Operations on a Shopping Cart
    class ShoppingCart
    {
    /** @var ShoppingCartItems[] */
    private $items;
    public function getTotalPrice() { ... }
    public function sortByCategories() { ... }
    public function issueReceipt(PDFGenerator $gen) { ... }
    }
    Implementing all these operations in the ShoppingCart class will
    bloat it at some point. Also, adding new operations requires to
    change the class body and will increase responsabilities and
    coupling with other dependencies.

    View Slide

  72. Make the Shopping Cart Visitable
    class ShoppingCart
    {
    // ...
    function accept(ShoppingCartVisitorInterface $visitor)
    {
    $visitor->visit($this);
    }
    }
    The first step is to make the ShoppingCart instance accept a
    ShoppingCartVisitorInterface implementation. The latter will then visit
    the shopping cart object that injects itself into the visit() method.

    View Slide

  73. Computing the Total Price of the Cart
    class TotalPriceCartVisitor implements ShoppingCartVisitorInterface
    {
    private $total;
    public function __construct()
    {
    $this->total = Money::fromString('EUR 0');
    }
    public function visit(ShoppingCart $cart)
    {
    foreach ($cart->getItems() as $item) {
    $this->total = $this->total->add($item->getTotalPrice());
    }
    }
    public function getTotalPrice()
    {
    return $this->total;
    }
    }

    View Slide

  74. Computing the Total Price of the Cart
    $cart = new ShoppingCart();
    // ...
    $pricerVisitor = new TotalPriceCartVisitor();
    $cart->accept($pricerVisitor);
    $totalPrice = $pricerVisitor->getTotalPrice();
    Computing the total price of the whole shopping cart is
    as simple as passing the visitor to the shopping cart
    instance.

    View Slide

  75. Sorting the Cart Items by Categories
    class SortCartItemsVisitor implements ShoppingCartVisitorInterface
    {
    private $sortedList;
    public function visit(ShoppingCart $cart)
    {
    foreach ($cart->getItems() as $item) {
    $this->sortedList[$item->getCategory()][] = $item;
    }
    ksort($this->sortedList);
    }
    public function getSortedCartItems()
    {
    return $this->sortedList;
    }
    }

    View Slide

  76. Sorting the Cart Items by Categories
    $cart = new ShoppingCart();
    // ...
    $sortVisitor = new SortCartItemsVisitor();
    $cart->accept($sortVisitor);
    print_r($sortVisitor->getSortedCartItems());
    Sorting the whole list of shopping cart items by product
    references categories is as simple as injecting the visitor
    into the ShoppingCart instance.

    View Slide

  77. Pros & Cons of Visitor Design Patterns
    Keep data objects and their operations separated from each other,
      Encourage developers to follow the SRP from SOLID,
      Encourage developers to follow the OCP from SOLID,
      Encourage losely coupled classes,
    Easy to extend the operations list by creating new visitors.
    Visitors must be mutable,
    Visitors may have to be updated if visitee changes,
    Potential edge cases if visitors change the state of the visitee,
    Cannot use polymorphism in visitors with PHP.

    View Slide

  78. Merci
    AFSY 2017 – Bordeaux – Hugo Hamon
    https://www.flickr.com/photos/64667396@N00/1668455127/sizes/o/

    View Slide