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

Confoo Vancouver - Design Patterns

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 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.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

December 06, 2017
Tweet

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/  
  2. Hugo Hamon

  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
  4. https://speakerdeck.com/hhamon

  5. Introduction

  6. SOLID Principles

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

    Inversion
  8. What are Design Patterns?

  9. In software design, a design pattern is an abstract generic

    solution to solve a particular redundant problem.
  10. Creational Abstract Factory Builder Factory Method Prototype Singleton Creational design

    patterns are responsible for encapsulating the algorithms for producing and assembling objects. Patterns
  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
  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
  13. What are the pros & cons of design patterns?

  14.   Communication   Code Testability Maintainability Loose Coupling   …

      Hard to Teach   Hard to Learn   Hard to Apply   Entry Barrier   …
  15. Patterns are not always the holly grail!!!

  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.
  17. Tailored for hierarchical data structures Family Tree File Explorer Organization

    Chart Content Management Book XML Parsing Navigation Bars etc. Web Forms Products Combos
  18. None
  19. Loading Routes Definitions from Configuration Files

  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]; } }
  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;
  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'));
  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; } }
  24. <!–- config/routes.xml -> <?xml version="1.0" encoding="UTF-8"?> <routes> <route name="get_contact" path="/contact"

    methods="GET" controller="App\Action\ContactDisplay"/> <!-- ... -> </routes>
  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; } }
  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'));
  27. What if the file loader must be chosen at runtime?

  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');
  29. class DelegatingLoader implements FileLoaderInterface { private $loaders = []; public

    function add(FileLoaderInterface $loader): void { $this->loaders[] = $loader; } // ... }
  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; } } }
  31. Builder Builder pattern separates the construction of a complex object

    from its representation so that the same construction process can create different representations.
  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'));
  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); } }
  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); } }
  35. Decorator Decorator pattern enables to dynamically attach new responsabilities to

    an object without changing its class.
  36. Decorator leverages Object Composition over Class Inheritance.

  37. None
  38. $instance = new OtherConcreteDecorator( new SomeConcreteDecorator( new ConcreteDecorator( new DecoratedComponent()

    ) ) ); $instance->operation();
  39. http://stackphp.com/

  40. Simple Object Decoration Example

  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; } }
  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; } }
  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(...));
  44. Making Infinite Object Responsabilities Combinations

  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.
  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.
  47. None
  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; }
  49. Modeling Coupon Objects Classes

  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
  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
  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)); } }
  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
  54. Modeling Coupon Restrictions as Coupon Decorators

  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); } }
  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); } }
  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'); );
  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); } }
  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') );
  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); } }
  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) );
  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('jsmith@example.com'); $order = new \Shop\Order($customer, new EUR('200.00')); // Apply discount coupon on the order $discountedAmount = $coupon->applyDiscount($order);
  63. Easy to setup Infinite combinations Unchanged existing code Loose coupling

      SOLID compliant   …   Large objects graphs   Proxy objects   Fluent interfaces   …
  64. Abstract Factory Abstract Factory provides an interface for creating families

    of related or dependent objects without specifying their concrete classes.
  65. 2016 - Twig 1.x 2015 – Symfony 3.0 2012 –

    Symfony 2.3
  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
  67. First Implementation Attempt…

  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)); // ... } }
  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); } }
  70. None
  71. Introducing an Abstract Factory Implementation

  72. None
  73. None
  74. Defining all main common interfaces

  75. namespace Certification; interface CertificationFactoryInterface { public function createEligibilityChecker() : CertificationEligibilityCheckerInterface;

    public function createTicketPricer() : CertificationTicketPricerInterface; public function createAuthority() : CertificationAuthorityInterface; }
  76. namespace Certification; use SebastianBergmann\Money\Money; interface CertificationTicketPricerInterface { public function getUnitPrice(string

    $country): Money; public function getTotalPrice(string $country, int $quantity): Money; }
  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; }
  78. Varying Certifications Tickets Pricing

  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
  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); } }
  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; } }
  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; } }
  83. Varying Certifications Authorities

  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) { // ... }
  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'; } }
  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; } // ... }
  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); } // ... }
  88. Implementing Concrete Factories

  89. None
  90. None
  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); } }
  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); } }
  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();
  94. The Giga Factory

  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); } }
  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); } }
  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; } }
  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(); } }