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 Slide

  2. Hugo Hamon

    View Slide

  3. 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 Slide

  4. https://speakerdeck.com/hhamon

    View Slide

  5. Introduction

    View Slide

  6. SOLID
    Principles

    View Slide

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

    View Slide

  8. What are Design
    Patterns?

    View Slide

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

    View Slide

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

    View Slide

  11. 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 Slide

  12. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 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 Slide

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

    View Slide

  18. View Slide

  19. Loading Routes
    Definitions from
    Configuration Files

    View Slide

  20. 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 Slide

  21. # 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 Slide

  22. 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 Slide

  23. 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 Slide




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

    View Slide

  25. 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 Slide

  26. 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 Slide

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

    View Slide

  28. 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 Slide

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

    View Slide

  30. 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 Slide

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

    View Slide

  32. $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 Slide

  33. 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 Slide

  34. 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 Slide

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

    View Slide

  36. Decorator leverages
    Object Composition
    over Class
    Inheritance.

    View Slide

  37. View Slide

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

    View Slide

  39. http://stackphp.com/

    View Slide

  40. Simple Object
    Decoration
    Example

    View Slide

  41. // ...
    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 Slide

  42. 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 Slide

  43. $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 Slide

  44. Making Infinite Object
    Responsabilities
    Combinations

    View Slide

  45. 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 Slide

  46. 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 Slide

  47. View Slide

  48. 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 Slide

  49. Modeling Coupon
    Objects Classes

    View Slide

  50. 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 Slide

  51. 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 Slide

  52. 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 Slide

  53. 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 Slide

  54. Modeling Coupon
    Restrictions as
    Coupon Decorators

    View Slide

  55. 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 Slide

  56. 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 Slide

  57. 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 Slide

  58. 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 Slide

  59. 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 Slide

  60. // ...
    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 Slide

  61. 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 Slide

  62. // 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  66. 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 Slide

  67. First
    Implementation
    Attempt…

    View Slide

  68. 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 Slide

  69. 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 Slide

  70. View Slide

  71. Introducing an
    Abstract Factory
    Implementation

    View Slide

  72. View Slide

  73. View Slide

  74. Defining all main
    common interfaces

    View Slide

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

    View Slide

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

    View Slide

  77. 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 Slide

  78. Varying
    Certifications
    Tickets Pricing

    View Slide

  79. $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 Slide

  80. 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 Slide

  81. 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 Slide

  82. 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 Slide

  83. Varying
    Certifications
    Authorities

    View Slide

  84. 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 Slide

  85. 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 Slide

  86. 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 Slide

  87. 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 Slide

  88. Implementing
    Concrete
    Factories

    View Slide

  89. View Slide

  90. View Slide

  91. 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 Slide

  92. 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 Slide

  93. $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 Slide

  94. The Giga
    Factory

    View Slide

  95. 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 Slide

  96. // ...
    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 Slide

  97. 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 Slide

  98. 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 Slide