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

Confoo Vancouver - Design Patterns

Hugo Hamon
December 06, 2017

Confoo Vancouver - Design Patterns

Design patterns are conceptual solutions to solve common redundant problems in software engineering. However, learning them is not easy as litterature or tutorials on the Internet often introduce them with theorical examples. This talk gives you a slightly different approach by introducing design patterns with practical code samples to solve real world problems. The talk focuses on the Abstract Factory, Builder, Decorator and Composite patterns.

Hugo Hamon

December 06, 2017
Tweet

More Decks by Hugo Hamon

Other Decks in Programming

Transcript

  1. Solving Real World
    Problems with
    Design Patterns.
    Confoo 2017 / Dec. 6th / Vancouver / Canada
    Hugo Hamon
    h"ps://www.flickr.com/photos/maelick/15302490013/  

    View full-size slide

  2. Hugo Hamon
    Senior Software Developer
    15 years of PHP experience
    10 years of Symfony experience
    Conferences speaker
    @hhamon on social networks
    Books (co) author

    View full-size slide

  3. https://speakerdeck.com/hhamon

    View full-size slide

  4. Introduction

    View full-size slide

  5. SOLID
    Principles

    View full-size slide

  6. Single Responsability
    Open / Closed
    Liskov Substitution
    Interface Segregation
    Dependency Inversion

    View full-size slide

  7. What are Design
    Patterns?

    View full-size slide

  8. In software design, a
    design pattern is an
    abstract generic solution
    to solve a particular
    redundant problem.

    View full-size slide

  9. Creational
    Abstract Factory
    Builder
    Factory Method
    Prototype
    Singleton
    Creational design
    patterns are responsible
    for encapsulating the
    algorithms for producing
    and assembling objects.
    Patterns

    View full-size slide

  10. Structural
    Adapter
    Bridge
    Composite
    Decorator
    Facade
    Flyweight
    Proxy
    Structural design
    patterns organize
    classes in a way to
    separate their
    implementations from
    their interfaces.
    Patterns

    View full-size slide

  11. Behavioral
    Chain of Responsibility
    Command
    Interpreter
    Iterator
    Mediator
    Memento
    Observer
    State
    Strategy
    Template Method
    Visitor
    Behavioral design
    patterns organize
    objects to make them
    collaborate together
    while reducing their
    coupling.
    Patterns

    View full-size slide

  12. What are the
    pros & cons of
    design patterns?

    View full-size slide

  13.   Communication
      Code Testability
    Maintainability
    Loose Coupling
      …
      Hard to Teach
      Hard to Learn
      Hard to Apply
      Entry Barrier
      …

    View full-size slide

  14. Patterns are not always
    the holly grail!!!

    View full-size slide

  15. Composite
    Composite pattern enables to treat
    individual objects and groups of
    objects of the same type the same
    way with a uniform common interface.

    View full-size slide

  16. Tailored for hierarchical data structures
    Family Tree
    File Explorer
    Organization Chart
    Content Management
    Book
    XML Parsing
    Navigation Bars
    etc.
    Web Forms
    Products Combos

    View full-size slide

  17. Loading Routes
    Definitions from
    Configuration Files

    View full-size slide

  18. namespace Routing;
    use Routing\Exception\RouteNotFoundException;
    final class UrlMatcher implements UrlMatcherInterface
    {
    private $routes;
    public function __construct(RouteCollection $routes)
    {
    $this->routes = $this->sortByPath($routes);
    }
    public function match(Request $request): array
    {
    if (!isset($this->routes[$path = $request->getPathInfo()])) {
    throw RouteNotFoundException::forUnsupportedPath($path);
    }
    return $this->routes[$path];
    }
    }

    View full-size slide

  19. # config/routes.php
    use Routing\Route;
    use Routing\RouteCollection;
    $routes = new RouteCollection();
    $route1 = new Route('/contact', 'App\\Action\\ContactDisplay');
    $route1->setMethods(['GET']);
    $routes->add('get_contact', $route1);
    $route2 = new Route('/contact', 'App\\Action\\ContactProcess');
    $route2->setMethods(['POST']);
    $routes->add('post_contact', $route2);
    return $routes;

    View full-size slide

  20. use Routing\UrlMatcher;
    use Routing\Loader\PhpFileLoader;
    $loader = new PhpFileLoader();
    $routes = $loader->load('config/routes.php');
    $router = new UrlMatcher($routes);
    $infos = $router->match(new Request('GET', '/home'));

    View full-size slide

  21. namespace Routing\Loader;
    use Routing\Exception\UnsupportedFileException;
    use Routing\RouteCollection;
    class PhpFileLoader implements FileLoaderInterface
    {
    public function load(string $path): RouteCollection
    {
    if (!file_exists($path)) {
    throw UnsupportedFileException::forUnreadableFile($path);
    }
    if ('php' !== pathinfo($path, PATHINFO_EXTENSION)) {
    throw UnsupportedFileException::forUnsupportedType($path, 'php');
    }
    $routes = require $path;
    if (!$routes instanceof RouteCollection) {
    throw UnsupportedFileException::forUnexpectedData($path, $routes);
    }
    return $routes;
    }
    }

    View full-size slide




  22. name="get_contact"
    path="/contact"
    methods="GET"
    controller="App\Action\ContactDisplay"/>

    View full-size slide

  23. class XmlFileLoader implements FileLoaderInterface
    {
    public function load(string $path): RouteCollection
    {
    // ...
    try {
    $xml = new \SimpleXMLElement(file_get_contents($path));
    } catch (\Exception $e) {
    throw UnsupportedFileException::forUnreadableFile($path, $e);
    }
    $routes = new RouteCollection();
    foreach ($xml->route as $route) {
    $routes->add($route['name'], $this->makeRoute($route));
    }
    return $routes;
    }
    }

    View full-size slide

  24. use Routing\UrlMatcher;
    use Routing\Loader\XmlFileLoader;
    $loader = new XmlFileLoader();
    $routes = $loader->load('config/routes.xml');
    $router = new UrlMatcher($routes);
    $infos = $router->match(new Request('GET', '/home'));

    View full-size slide

  25. What if the file
    loader must be
    chosen at runtime?

    View full-size slide

  26. use Routing\Loader\DelegatingLoader;
    use Routing\Loader\PhpFileLoader;
    use Routing\Loader\XmlFileLoader;
    $loader = new DelegatingLoader();
    $loader->add(new PhpFileLoader());
    $loader->add(new XmlFileLoader());
    $routes = $loader->load('config/routes.xml');
    $routes = $loader->load('config/routes.php');

    View full-size slide

  27. class DelegatingLoader implements FileLoaderInterface
    {
    private $loaders = [];
    public function add(FileLoaderInterface $loader): void
    {
    $this->loaders[] = $loader;
    }
    // ...
    }

    View full-size slide

  28. class DelegatingLoader implements FileLoaderInterface
    {
    // ...
    public function load(string $path): RouteCollection
    {
    foreach ($this->loaders as $loader) {
    if ($routes = $this->tryLoader($path, $loader)) {
    return $routes;
    }
    }
    throw UnsupportedFileException::forUnsupportedType($path);
    }
    private function tryLoader(string $path, FileLoaderInterface $loader): ?RouteCollection
    {
    try {
    return $loader->load($path);
    } catch (UnsupportedFileException $exception) {
    return null;
    }
    }
    }

    View full-size slide

  29. Builder
    Builder pattern separates the construction
    of a complex object from its representation
    so that the same construction process can
    create different representations.

    View full-size slide

  30. $matcher = UrlMatcherBuilder::create()
    ->load('config/routes.xml')
    ->add('/home', 'homepage', 'App\\Action\\Homepage')
    ->add('/about', 'about', 'App\\Action\\About')
    ->load('/config/routes.php')
    ->build()
    ;
    $infos = $router->match(new Request('GET', '/home'));

    View full-size slide

  31. final class UrlMatcherBuilder implements UrlMatcherBuilderInterface
    {
    private $loader;
    private $routes;
    public function __construct(FileLoaderInterface $loader)
    {
    $this->loader = $loader;
    $this->routes = new RouteCollection();
    }
    public static function create(): self
    {
    $loader = new DelegatingLoader();
    $loader->add(new XmlFileLoader());
    $loader->add(new PhpFileLoader());
    return new self($loader);
    }
    }

    View full-size slide

  32. class UrlMatcherBuilder
    {
    public function load(string $path): self
    {
    $this->routes->merge($this->loader->load($path));
    return $this;
    }
    public function add(string $path, string $name, callable $callback): self
    {
    $this->routes->add(new Route($path, $name, $callback));
    return $this;
    }
    public function build(): UrlMatcherInterface
    {
    return new UrlMatcher($this->routes);
    }
    }

    View full-size slide

  33. Decorator
    Decorator pattern enables to
    dynamically attach new
    responsabilities to an object without
    changing its class.

    View full-size slide

  34. Decorator leverages
    Object Composition
    over Class
    Inheritance.

    View full-size slide

  35. $instance = new OtherConcreteDecorator(
    new SomeConcreteDecorator(
    new ConcreteDecorator(
    new DecoratedComponent()
    )
    )
    );
    $instance->operation();

    View full-size slide

  36. http://stackphp.com/

    View full-size slide

  37. Simple Object
    Decoration
    Example

    View full-size slide

  38. // ...
    use Psr\Log\LoggerInterface;
    class LoggableUrlMatcher implements UrlMatcherInterface
    {
    private $urlMatcher;
    private $logger;
    public function __construct(
    UrlMatcherInterface $urlMatcher,
    LoggerInterface $logger
    ) {
    $this->urlMatcher = $urlMatcher;
    $this->logger = $logger;
    }
    }

    View full-size slide

  39. class LoggableUrlMatcher implements UrlMatcherInterface
    {
    // ...
    public function match(Request $request): array
    {
    $this->logger->info(sprintf('Matching path "%s".', $request->getPathInfo()));
    try {
    $infos = $this->urlMatcher->match($request);
    $this->logger->info(sprintf('Matched route "%s".', $infos['_route']));
    } catch (RoutingException $e) {
    $this->logger->info($e->getMessage());
    throw $e;
    }
    return $infos;
    }
    }

    View full-size slide

  40. $loader = new PhpFileLoader();
    $routes = $loader->load('/config/routes.php');
    $matcher = new LoggableUrlMatcher(
    new UrlMatcher($routes),
    new Logger('/tmp/app.log')
    );
    $matcher->match(new Request(...));

    View full-size slide

  41. Making Infinite Object
    Responsabilities
    Combinations

    View full-size slide

  42. Coupon Codes Management
    Consider an e-commerce website
    offering coupon codes for customers
    to reduce their orders. Coupons are
    accepted and will discount an order
    total amount if their restrictions
    constraints are validated.

    View full-size slide

  43. Coupons Restrictions
    ü  Limited lifetime period,
    ü  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 full-size slide

  44. interface CouponInterface
    {
    public function getCode(): string;
    /**
    * Returns the new total amount after the coupon has been
    * applied on the given order.
    *
    * @param OrderableInterface $order The order to discount
    *
    * @return Money The new order total amount
    *
    * @throws CouponException When coupon is not applicable
    */
    public function applyDiscount(OrderableInterface $order): Money;
    }

    View full-size slide

  45. Modeling Coupon
    Objects Classes

    View full-size slide

  46. class ValueCoupon implements CouponInterface
    {
    private $code;
    private $discount;
    public function __construct(string $code, Money $discount)
    {
    $this->code = $code;
    $this->discount = $discount;
    }
    public function getCode(): string
    {
    return $this->code;
    }
    public function applyDiscount(OrderableInterface $order): Money
    {
    return $order->getTotalAmount()->subtract($this->discount);
    }
    }
    Modelling a Value Coupon

    View full-size slide

  47. class RateCoupon implements CouponInterface
    {
    private $code;
    private $rate;
    public function __construct(string $code, float $rate)
    {
    Assertion::greaterThan($rate, 0);
    Assertion::lowerThanOrEqual($rate, 1);
    $this->code = $code;
    $this->rate = $rate;
    }
    }
    Modelling a Rate Coupon

    View full-size slide

  48. class RateCoupon implements CouponInterface
    {
    // ...
    public function getCode()
    {
    return $this->code;
    }
    public function applyDiscount(OrderableInterface $order): Money
    {
    $amount = $order->getTotalAmount();
    return $amount->subtract($amount->multiply($this->rate));
    }
    }

    View full-size slide

  49. abstract class CouponDecorator implements CouponInterface
    {
    protected $coupon;
    public function __construct(CouponInterface $coupon)
    {
    $this->coupon = $coupon;
    }
    public function getCode(): string
    {
    return $this->coupon->getCode();
    }
    protected function createCouponException(string $message, \Throwable $previous = null)
    {
    return new CouponException($message, 0, $previous);
    }
    }
    Modelling a Coupon Decorator

    View full-size slide

  50. Modeling Coupon
    Restrictions as
    Coupon Decorators

    View full-size slide

  51. class LimitedLifetimeCoupon extends CouponDecorator
    {
    private $period;
    function __construct(CouponInterface $coupon, DateTimePeriod $period)
    {
    parent::__construct($coupon);
    $this->period = $period;
    }
    public function applyDiscount(OrderableInterface $order): Money
    {
    // ... Add restriction logic here.
    return $this->coupon->applyDiscount($order);
    }
    }

    View full-size slide

  52. class LimitedLifetimeCoupon extends CouponDecorator
    {
    // ...
    public function applyDiscount(OrderableInterface $order): Money
    {
    if (!$this->period->isStarted()) {
    throw $this->createCouponException(sprintf(
    'Coupon is usable from %s.',
    $this->period->getStartDate()
    ));
    }
    if ($this->period->isFinished()) {
    throw $this->createCouponException(sprintf(
    'Coupon was valid until %s.',
    $this->period->getDueDate()
    ));
    }
    return $this->coupon->applyDiscount($order);
    }
    }

    View full-size slide

  53. use SebastianBergmann\Money\Money;
    use Shop\Discount\LimitedLifetimeCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Coupon offers a discount value of 20 €.
    $coupon1 = new LimitedLifetimeCoupon(
    new ValueCoupon('3s2h7pd65s', Money::fromString('20.00', EUR')),
    DateTimePeriod::fromString('FROM 2017-12-01 TO 2017-12-31');
    );
    // Coupon offers 25% off.
    $coupon2 = new LimitedLifetimeCoupon(
    new RateCoupon('76cqa6qr19', 0.25),
    DateTimePeriod::fromString('FROM 2017-12-01 TO 2017-12-31');
    );

    View full-size slide

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

    View full-size slide

  55. use SebastianBergmann\Money\Money;
    use Shop\Discount\MinimumPurchaseAmountCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Get 20 € off if total amount is greater than 300 €
    $coupon1 = new MinimumPurchaseAmountCoupon(
    new ValueCoupon('3s2h7pd65s', new EUR('20.00')),
    new EUR('300.00')
    );
    // Get 25% off if total amount is greater than 300 €
    $coupon2 = new MinimumPurchaseAmountCoupon(
    new RateCoupon('76cqa6qr19', 0.25),
    new EUR('300.00')
    );

    View full-size slide

  56. // ...
    class CustomerFirstOrderCoupon extends CouponDecorator
    {
    public function applyDiscount(OrderableInterface $order)
    {
    $customer = $order->getCustomer();
    if ($customer->hasPastOrders()) {
    throw $this->createCouponException(
    'Customer already has past orders.’
    );
    }
    return $this->coupon->applyDiscount($order);
    }
    }

    View full-size slide

  57. use SebastianBergmann\Money\Money;
    use Shop\Discount\CustomerFirstOrderCoupon;
    use Shop\Discount\RateCoupon;
    use Shop\Discount\ValueCoupon;
    // Get 20 € off on the first order
    $coupon1 = new CustomerFirstOrderCoupon(
    new ValueCoupon(
    '3s2h7pd65s',
    new EUR('20.00')
    )
    );
    // Get 25% off on the first order
    $coupon2 = new CustomerFirstOrderCoupon(
    new RateCoupon('76cqa6qr19', 0.25)
    );

    View full-size slide

  58. // Create the special coupon
    $coupon = new CustomerFirstOrderCoupon(
    new MinimumPurchaseAmountCoupon(
    new LimitedLifetimeCoupon(
    new ValueCoupon('3s2h7pd65s', new EUR('20.00')),
    DateTimePeriod::until('+60 days')
    ),
    new EUR('170.00')
    )
    );
    // Create the order instance
    $customer = new \Shop\Customer('[email protected]');
    $order = new \Shop\Order($customer, new EUR('200.00'));
    // Apply discount coupon on the order
    $discountedAmount = $coupon->applyDiscount($order);

    View full-size slide

  59. Easy to setup
    Infinite combinations
    Unchanged existing code
    Loose coupling
      SOLID compliant
      …
      Large objects graphs
      Proxy objects
      Fluent interfaces
      …

    View full-size slide

  60. Abstract Factory
    Abstract Factory provides an
    interface for creating families of
    related or dependent objects without
    specifying their concrete classes.

    View full-size slide

  61. 2016 - Twig 1.x
    2015 – Symfony 3.0
    2012 – Symfony 2.3

    View full-size slide

  62. Both assessments have similarities
    -­‐ Symfony  2.3  /  3.0 Twig
    Pricing
    €250 (USA, Europe, UAE, Japan, etc.)
    €200 (Brazil, Tunisia, India, etc.)
    €149
    (no country restriction)
    Eligibility
    Conditions
    Candidate must be at least 18 y.o
    Candidate must not be Expert Certified
    Up to 2 sessions max per civil year
    No active exam registration
    1 week blank period between 2 exams
    Candidate must be at least 18 y.o
    Candidate must not be Expert Certified
    No active exam registration
    1 month blank period between 2 exams
    Levels
    Expert Symfony Developer
    Advanced Symfony Developer
    Expert Twig Web Designer

    View full-size slide

  63. First
    Implementation
    Attempt…

    View full-size slide

  64. class PlaceOrderCommandHandler
    {
    private $symfonyPricer;
    private $twigPricer;
    // ...
    public function handle(PlaceOrderCommand $command): void
    {
    $order = new Order();
    $order->setQuantity($command->getQuantity());
    $order->setUnitPrice($this->getUnitPrice($command));
    // ...
    }
    }

    View full-size slide

  65. class PlaceOrderCommandHandler
    {
    // ...
    private function getUnitPrice(PlaceOrderCommand $command): Money
    {
    $country = $command->getCountry();
    switch ($code = $command->getExamSeriesType()) {
    case 'TWXCE':
    return $this->twigPricer->getUnitPrice($country);
    case 'SFXCE':
    return $this->symfonyPricer->getUnitPrice($country);
    }
    throw new UnsupportedAssessmentException($code);
    }
    }

    View full-size slide

  66. Introducing an
    Abstract Factory
    Implementation

    View full-size slide

  67. Defining all main
    common interfaces

    View full-size slide

  68. namespace Certification;
    interface CertificationFactoryInterface
    {
    public function createEligibilityChecker()
    : CertificationEligibilityCheckerInterface;
    public function createTicketPricer()
    : CertificationTicketPricerInterface;
    public function createAuthority()
    : CertificationAuthorityInterface;
    }

    View full-size slide

  69. namespace Certification;
    use SebastianBergmann\Money\Money;
    interface CertificationTicketPricerInterface
    {
    public function getUnitPrice(string $country): Money;
    public function getTotalPrice(string $country, int $quantity): Money;
    }

    View full-size slide

  70. namespace Certification;
    use Certification\Domain\AssessmentResult;
    use Certification\Exception\CandidateNotCertifiedException;
    use Certification\Exception\UnsupportedAssessmentException;
    interface CertificationAuthorityInterface
    {
    /**
    * Returns the candidate's certification level.
    *
    * @throws CandidateNotCertifiedException
    * @throws UnsupportedAssessmentException
    */
    public function getCandidateLevel(AssessmentResult $result): string;
    }

    View full-size slide

  71. Varying
    Certifications
    Tickets Pricing

    View full-size slide

  72. $pricer = new \Certification\Symfony\TicketPricer(
    Money::fromString('250', 'EUR'),
    Money::fromString('200', 'EUR')
    );
    $price = $pricer->getUnitPrice('FR'); // €250
    $price = $pricer->getUnitPrice('TN'); // €200
    $pricer = new \Certification\Twig\TicketPricer(
    Money::fromString('149', 'EUR')
    );
    $price = $pricer->getUnitPrice('FR'); // €149
    $price = $pricer->getUnitPrice('TN'); // €149

    View full-size slide

  73. namespace Certification;
    use SebastianBergmann\Money\Money;
    abstract class AbstractTicketPricer
    implements CertificationTicketPricerInterface
    {
    /**
    * Returns the total price for the given quantity.
    */
    public function getTotalPrice(string $country, int $quantity): Money
    {
    if ($quantity < 1) {
    throw new \InvalidTicketQuantityException($quantity);
    }
    return $this->getUnitPrice($country)->multiply($quantity);
    }
    }

    View full-size slide

  74. namespace Certification\Twig;
    use SebastianBergmann\Money\Money;
    use Certification\AbstractTicketPricer;
    class TicketPricer extends AbstractTicketPricer
    {
    private $price;
    public function __construct(Money $price)
    {
    $this->price = $price;
    }
    public function getUnitPrice(string $country): Money
    {
    return $this->price;
    }
    }

    View full-size slide

  75. namespace Certification\Symfony;
    use SebastianBergmann\Money\Money;
    use Certification\AbstractTicketPricer;
    class TicketPricer extends AbstractTicketPricer
    {
    private $regularPrice;
    private $discountPrice;
    public function __construct(Money $regularPrice, Money $discountPrice)
    {
    $this->regularPrice = $regularPrice;
    $this->discountPrice = $discountPrice;
    }
    public function getUnitPrice(string $country): Money
    {
    if (in_array($country, ['FR', 'US', 'DE', 'IT', 'CH', 'BE', /* … */])) {
    return $this->regularPrice;
    }
    return $this->discountPrice;
    }
    }

    View full-size slide

  76. Varying
    Certifications
    Authorities

    View full-size slide

  77. try {
    $assessment = AssessmentResult::fromCSV('SL038744,SF3CE,35');
    $authority = new \Certification\Symfony\Authority(20, 10);
    $symfonyLevel = $authority->getCandidateLevel($assessment);
    $assessment = AssessmentResult::fromCSV('SL038744,TW1CE,17');
    $authority = new \Certification\Twig\Authority(15);
    $twigLevel = $authority->getCandidateLevel($assessment);
    } catch (CandidateNotCertifiedException $e) {
    // ...
    } catch (UnsupportedAssessmentException $e) {
    // ...
    } catch (\Exception $e) {
    // ...
    }

    View full-size slide

  78. namespace Certification\Twig;
    use Certification\CertificationAuthorityInterface;
    use Certification\Domain\AssessmentResult;
    use Certification\Exception\CandidateNotCertifiedException;
    use Certification\Exception\UnsupportedAssessmentException;
    class Authority implements CertificationAuthorityInterface
    {
    // ...
    public function getCandidateLevel(AssessmentResult $result): string
    {
    if ('TW1CE' !== $assessmentID = $result->getAssessmentID()) {
    throw new UnsupportedAssessmentException($assessmentID);
    }
    if ($result->getScore() < $this->passingScore) {
    throw new CandidateNotCertifiedException($result->getCandidateID(), $assessmentID);
    }
    return 'expert';
    }
    }

    View full-size slide

  79. namespace Certification\Symfony;
    use Certification\CertificationAuthorityInterface;
    use Certification\Entity\AssessmentResult;
    use Certification\Exception\CandidateNotCertifiedException;
    use Certification\Exception\UnsupportedAssessmentException;
    class Authority implements CertificationAuthorityInterface
    {
    private $expertLevelPassingScore;
    private $advancedLevelPassingScore;
    public function __construct(int $expertScore, int $advancedScore)
    {
    $this->expertLevelPassingScore = $expertScore;
    $this->advancedLevelPassingScore = $advancedScore;
    }
    // ...
    }

    View full-size slide

  80. class Authority implements CertificationAuthorityInterface
    {
    public function getCandidateLevel(AssessmentResult $result): string
    {
    $assessmentID = $result->getAssessmentID();
    if (!in_array($assessmentID, ['SF2CE', 'SF3CE'])) {
    throw new UnsupportedAssessmentException($assessmentID);
    }
    $score = $result->getScore();
    if ($score >= $this->expertLevelPassingScore) {
    return 'expert';
    }
    if ($score >= $this->advancedLevelPassingScore) {
    return 'advanced';
    }
    throw new CandidateNotCertifiedException($result->getCandidateID(), $assessmentID);
    }
    // ...
    }

    View full-size slide

  81. Implementing
    Concrete
    Factories

    View full-size slide

  82. namespace Certification\Symfony;
    use SebastianBergmann\Money\Money;
    use Certification\CertificationAuthorityInterface;
    use Certification\CertificationFactoryInterface;
    // ...
    class CertificationFactory implements CertificationFactoryInterface
    {
    // ...
    public function createTicketPricer(): CertificationTicketPricerInterface
    {
    return new TicketPricer(
    Money::fromString('250', 'EUR'),
    Money::fromString('200', 'EUR')
    );
    }
    public function createAuthority(): CertificationAuthorityInterface
    {
    return new Authority(20, 10);
    }
    }

    View full-size slide

  83. namespace Certification\Twig;
    use SebastianBergmann\Money\Money;
    use Certification\CertificationAuthorityInterface;
    use Certification\CertificationFactoryInterface;
    // ...
    class CertificationFactory implements CertificationFactoryInterface
    {
    // ...
    public function createTicketPricer(): CertificationTicketPricerInterface
    {
    return new TicketPricer(Money::fromString('149', 'EUR'));
    }
    public function createAuthority(): CertificationAuthorityInterface
    {
    return new Authority(20);
    }
    }

    View full-size slide

  84. $assessmentFamily = 'SFXCE';
    // Choose the right factory
    switch ($assessmentFamily) {
    case 'TWXCE':
    $factory = new TwigCertificationFactory(...);
    break;
    case 'SFXCE':
    $factory = new SymfonyCertificationFactory(...);
    break;
    default:
    throw new UnsupportedAssessmentFamilyException($assessmentFamily);
    }
    // Request and use the factory services
    $pricer = $factory->createTicketPricer();
    $checker = $factory->createEligibilityChecker();
    $authority = $factory->createAuthority();

    View full-size slide

  85. The Giga
    Factory

    View full-size slide

  86. namespace Certification;
    use Certification\Exception\UnsupportedAssessmentException;
    //...
    class CertificationFactoryFactory implements CertificationFactoryFactoryInterface
    {
    private $factories = [];
    public function getFactory(string $assessment): CertificationFactoryInterface
    {
    if (isset($this->factories[$assessment])) {
    return $this->factories[$assessment];
    }
    if ('TW1CE' === $assessment) {
    return $this->createTwigCertificationFactory($assessment);
    }
    if (in_array($assessment, ['SF2CE', 'SF3CE'], true)) {
    return $this->createSymfonyCertificationFactory($assessment);
    }
    throw new UnsupportedAssessmentException($assessment);
    }
    }

    View full-size slide

  87. // ...
    use Certification\Symfony\CertificationFactory as SymfonyCertificationFactory;
    use Certification\Twig\CertificationFactory as TwigCertificationFactory;
    class CertificationFactoryFactory implements CertificationFactoryFactoryInterface
    {
    private $factories = [];
    public function getFactory(string $assessment): CertificationFactoryInterface
    {
    if (isset($this->factories[$assessment])) {
    return $this->factories[$assessment];
    }
    if ('TW1CE' === $assessment) {
    return $this->createTwigCertificationFactory($assessment);
    }
    if (in_array($assessment, ['SF2CE', 'SF3CE'], true)) {
    return $this->createSymfonyCertificationFactory($assessment);
    }
    throw new UnsupportedAssessmentException($assessment);
    }
    }

    View full-size slide

  88. class CertificationFactoryFactory implements CertificationFactoryFactoryInterface
    {
    // ...
    private function createTwigCertificationFactory(string $assessment)
    : CertificationFactoryInterface
    {
    $this->factories[$assessment] = $f = new TwigCertificationFactory(...);
    return $f;
    }
    private function createSymfonyCertificationFactory(string $assessment)
    : CertificationFactoryInterface
    {
    $this->factories[$assessment] = $f = new SymfonyCertificationFactory(...);
    return $f;
    }
    }

    View full-size slide

  89. class CertificationFactoryFactory implements CertificationFactoryFactoryInterface
    {
    // ...
    public function createCertificationTicketPricer(string $assessment)
    : CertificationTicketPricerInterface
    {
    return $this->getFactory($assessment)->createTicketPricer();
    }
    public function createCertificationAuthority(string $assessment)
    : CertificationAuthorityInterface
    {
    return $this->getFactory($assessment)->createCertificationAuthority();
    }
    public function createCertificationEligibilityChecker(string $assessment)
    : CertificationEligibilityCheckerInterface
    {
    return $this->getFactory($assessment)->createEligibilityChecker();
    }
    }

    View full-size slide