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

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/
  2. What are design patterns? In software design, a design pattern

    is an abstract generic solution to solve a particular redundant problem.
  3. Design Patterns Classification 23 « Gang of Four » Design

    Patterns ¤  Creational ¤  Structural ¤  Behavioral
  4. Benefits of Design Patterns ¤  Communication & vocabulary ¤  Testability

    ¤  Maintainance ¤  Extensibility ¤  Loose coupling
  5. 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!
  6. 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
  7. Creational Patterns Creational design patterns encapsulate and isolate the algorithms

    to create and initialize objects. Abstract Factory – Builder – Factory Method Lazy Initialization – Prototype – Singleton
  8. 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
  9. 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.
  10. 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); } }
  11. 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); } }
  12. 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.
  13. 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); } }
  14. 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); // ... } }
  15. 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.
  16. 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.
  17. 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";
  18. 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";
  19. 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 }
  20. 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); } }
  21. 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; } }
  22. 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.
  23. 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 }
  24. 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] ]; } }
  25. 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']); } }
  26. 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";
  27. 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.
  28. Structural Patterns Structural patterns organize classes and help separating the

    implementations from their interfaces. Adapter – Bridge – Composite – Decorator Facade – Flyweight – Proxy
  29. 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.
  30. 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)   …
  31. 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();
  32. 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.
  33. 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… }
  34. 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)); } }
  35. 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') );
  36. 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; } }
  37. 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(); } }
  38. 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(); } }
  39. 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 €
  40. 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 €
  41. 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 €
  42. 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.
  43. The Decorator Design Pattern Usage $instance = new OtherConcreteDecorator( new

    SomeConcreteDecorator( new ConcreteDecorator( new DecoratedComponent() ) ) ); $instance->operation();
  44. 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.
  45. 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.
  46. 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); } }
  47. 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)); } }
  48. 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); } }
  49. 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; } }
  50. 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); } }
  51. 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' );
  52. 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); } }
  53. 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 );
  54. 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);
  55. 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.
  56. 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
  57. 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.
  58. 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 } }
  59. 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.
  60. 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.
  61. 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).
  62. 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.
  63. 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.
  64. 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; } }
  65. 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.
  66. 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; } }
  67. 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.
  68. 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.