Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Decoupling with Design Patterns and Symfony2 DIC

5,824 views

Published on

How do you create applications with an incredible level of extendability without losing readability in the process? What if there's a way to separate concerns not only on the code, but on the service definition level? This talk will explore structural and behavioural patterns and ways to enrich them through tricks of powerful dependency injection containers such as Symfony2 DIC component.

Published in: Software
  • Hello! Get Your Professional Job-Winning Resume Here - Check our website! https://vk.cc/818RFv
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Decoupling with Design Patterns and Symfony2 DIC

  1. 1. Decoupling with Design Patterns and Symfony DIC
  2. 2. @everzet · Spent more than 7 years writing so!ware · Spent more than 4 years learning businesses · Now filling the gaps between the two as a BDD Practice Manager @Inviqa
  3. 3. behat 3 promise #1 (of 2): extensibility
  4. 4. “Extensibility is a so!ware design principle defined as a system’s ability to have new functionality extended, in which the system’s internal structure and data flow are minimally or not affected”
  5. 5. “So!ware entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”
  6. 6. behat 3 promise #2 (of 2): backwards compatibility
  7. 7. behat 3 - extensibility as the core concept - BC through extensibility
  8. 8. Symfony Bundles Behat extensions
  9. 9. Symfony Bundles & Behat extensions 1. Framework creates a temporary container 2. Framework asks the bundle to add its services 3. Framework merges all temporary containers 4. Framework compiles merged container
  10. 10. interface CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container * * @api */ public function process(ContainerBuilder $container); }
  11. 11. class YourSuperBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new YourCompilerPass()); } }
  12. 12. v3.0 v1.0 (extensibility solution v1)
  13. 13. challenge: behat as the most extensible test framework
  14. 14. pattern: observer
  15. 15. class HookDispatcher extends DispatchingService implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::BEFORE_SUITE => array('dispatchHooks', 10), EventInterface::AFTER_SUITE => array('dispatchHooks', 10), EventInterface::BEFORE_FEATURE => array('dispatchHooks', 10), ... ); } public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } } ... }
  16. 16. class HooksCarrierEvent extends Event implements LifecycleEventInterface { public function addHook(HookInterface $hook) { $this->hooks[] = $hook; } public function getHooksForEvent(Event $event) { return array_filter( $this->hooks, function ($hook) use ($event) { $eventName = $event->getName(); if ($eventName !== $hook->getEventName()) { return false; } return $hook; } ); } ... }
  17. 17. class DictionaryReader implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::LOAD_HOOKS => array('loadHooks', 0), ... ); } public function loadHooks(HooksCarrierEvent $event) { foreach ($this->read($event->getSuite(), $event->getContextPool()) as $callback) { if ($callback instanceof HookInterface) { $event->addHook($callback); } } } ... }
  18. 18. extension point
  19. 19. <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id="event_dispatcher" class="SymfonyComponentEventDispatcherEventDispatcher"/> <service id="hook.hook_dispatcher" class="BehatBehatHookEventSubscriberHookDispatcher"> <argument type="service" id="event_dispatcher"/> <tag name="event_subscriber"/> </service> <service id="context.dictionary_reader" class="BehatBehatContextEventSubscriberDictionaryReader"> <tag name="event_subscriber"/> </service> </services> </container>
  20. 20. class EventSubscribersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $dispatcherDefinition = $container->getDefinition('event_dispatcher'); foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) { $dispatcherDefinition->addMethodCall('addSubscriber', array(new Reference($id))); } } }
  21. 21. where event dispatcher / observer is useful?
  22. 22. pub/sub as an architectural choice
  23. 23. “Coupling is a degree to which each program module relies on each one of the other modules”
  24. 24. “Cohesion is a degree to which the elements of a module belong together”
  25. 25. “Coupling is a degree to which each program module relies on each one of the other modules” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
  26. 26. “Cohesion is a degree to which the elements of a module belong together” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
  27. 27. Coupling ↓ Cohesion ↑
  28. 28. scratch that
  29. 29. v3.0 v2.0 (extensibility solution v2)
  30. 30. There is no single solution for extensibility. Because extensibility is not a single problem
  31. 31. framework extensions Since v2.5 behat has some very important extensions: 1. MinkExtension 2. Symfony2Extension
  32. 32. problem: there are multiple possible algorithms for a single responsibility
  33. 33. pattern: delegation loop
  34. 34. final class EnvironmentManager { private $handlers = array(); public function registerEnvironmentHandler(EnvironmentHandler $handler) { $this->handlers[] = $handler; } public function buildEnvironment(Suite $suite) { foreach ($this->handlers as $handler) { ... } } public function isolateEnvironment(Environment $environment, $testSubject = null) { foreach ($this->handlers as $handler) { ... } } }
  35. 35. interface EnvironmentHandler { public function supportsSuite(Suite $suite); public function buildEnvironment(Suite $suite); public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null); public function isolateEnvironment(Environment $environment, $testSubject = null); }
  36. 36. extension point
  37. 37. <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“environment.manager” class="BehatTestworkEnvironmentEnvironmentManager” /> <service id=“behat.context.environment.handler” class=“BehatBehatContextEnvironmentContextEnvironmentHandler”> <tag name=“environment.handler”/> </service> </services> </container>
  38. 38. final class EnvironmentHandlerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘environment.handler’); $definition = $container->getDefinition(‘environment.manager’); foreach ($references as $reference) { $definition->addMethodCall('registerEnvironmentHandler', array($reference)); } } }
  39. 39. where delegation loop is useful?
  40. 40. behat testers There are 5 testers in behat core: 1. FeatureTester 2. ScenarioTester 3. OutlineTester 4. BackgroundTester 5. StepTester
  41. 41. behat testers Behat needs to provide you with: · Hooks · Events
  42. 42. problem: we need to dynamically extend the core testers behaviour
  43. 43. pattern: decorator
  44. 44. final class RuntimeScenarioTester implements ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { return new SuccessfulSetup(); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { ... } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { return new SuccessfulTeardown(); } }
  45. 45. interface ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result); }
  46. 46. final class EventDispatchingScenarioTester implements ScenarioTester { public function __construct(ScenarioTester $baseTester, EventDispatcherInterface $eventDispatcher) { $this->baseTester = $baseTester; $this->eventDispatcher = $eventDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { $event = new BeforeScenarioTested($env, $feature, $scenario); $this->eventDispatcher->dispatch($this->beforeEventName, $event); $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); return $setup; } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $event = new AfterScenarioTested($env, $feature, $scenario, $result, $teardown); $this->eventDispatcher->dispatch($event); return $teardown; } }
  47. 47. final class HookableScenarioTester implements ScenarioTester { public function __construct(ScenarioTester $baseTester, HookDispatcher $hookDispatcher) { $this->baseTester = $baseTester; $this->hookDispatcher = $hookDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($setup); return new HookedSetup($setup, $hookCallResults); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($teardown); return new HookedTeardown($teardown, $hookCallResults); } }
  48. 48. extension point
  49. 49. <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“tester.scenario” class="BehatBehatTesterScenarioTester” /> <service id=“hooks.tester.scenario” class=“BehatBehatHooksTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“100”/> </service> <service id=“events.tester.scenario” class=“BehatBehatEventsTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“200”/> </service> </services> </container>
  50. 50. final class ScenarioTesterWrappersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->findAndReorderTaggedServices($container, ‘tester.scenario_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . '.inner'; // This logic is based on SymfonyComponentDependencyInjectionCompilerDecoratorServicePass $definition = $container->getDefinition(‘tester.scenario’); $container->setDefinition($renamedId, $definition); $container->setAlias('tester.scenario', new Alias($id, $public)); $wrappingService = $container->getDefinition($id); $wrappingService->replaceArgument(0, new Reference($renamedId)); } } ... }
  51. 51. where decorator is useful?
  52. 52. behat output Behat has a very simple output:
  53. 53. behat output Until you start using backgrounds:
  54. 54. behat output And throwing exceptions from their hooks:
  55. 55. problem: we need to add behaviour to complex output logic
  56. 56. pattern: observer
  57. 57. pattern: chain of responsibility
  58. 58. pattern: composite
  59. 59. final class NodeEventListeningFormatter implements Formatter { public function __construct(EventListener $listener) { $this->listener = $listener; } public static function getSubscribedEvents() { return array(TestworkEventDispatcher::BEFORE_ALL_EVENTS => 'listenEvent'); } public function listenEvent(Event $event, $eventName = null) { $eventName = $eventName ?: $event->getName(); $this->listener->listenEvent($this, $event, $eventName); } }
  60. 60. final class ChainEventListener implements EventListener, Countable, IteratorAggregate { private $listeners; public function __construct(array $listeners) { $this->listeners = $listeners; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { foreach ($this->listeners as $listener) { $listener->listenEvent($formatter, $event, $eventName); } } ... }
  61. 61. Event listeners Behat has 2 types of listeners: 1. Printers 2. Flow controllers
  62. 62. final class StepListener implements EventListener { public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->captureScenarioOnScenarioEvent($event); $this->forgetScenarioOnAfterEvent($eventName); $this->printStepSetupOnBeforeEvent($formatter, $event); $this->printStepOnAfterEvent($formatter, $event); } ... }
  63. 63. How do backgrounds work?
  64. 64. class FirstBackgroundFiresFirstListener implements EventListener { public function __construct(EventListener $descendant) { $this->descendant = $descendant; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->flushStatesIfBeginningOfTheFeature($eventName); $this->markFirstBackgroundPrintedAfterBackground($eventName); if ($this->isEventDelayedUntilFirstBackgroundPrinted($event)) { $this->delayedUntilBackgroundEnd[] = array($event, $eventName); return; } $this->descendant->listenEvent($formatter, $event, $eventName); $this->fireDelayedEventsOnAfterBackground($formatter, $eventName); } }
  65. 65. where composite and CoR are useful?
  66. 66. interface StepTester { public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result); }
  67. 67. problem: we need to introduce backwards incompatible change into the API
  68. 68. pattern: adapter
  69. 69. interface ScenarioStepTester { public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result); }
  70. 70. final class StepToScenarioTesterAdapter implements ScenarioStepTester { public function __construct(StepTester $stepTester) { ... } public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->setUp($env, $feature, $step, $skip); } public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->test($env, $feature, $step, $skip); } public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result) { return $this->stepTester-> tearDown($env, $feature, $step, $skip); } }
  71. 71. final class StepTesterAdapterPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘tester.step_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . ‘.adaptee’; $adapteeDefinition = $container->getDefinition($id); $reflection = new ReflectionClass($adapteeDefinition->getClass()); if (!$reflection->implementsInterface(‘StepTester’)) { return; } $container->removeDefinition($id); $container->setDefinition( $id, new Definition(‘StepToScenarioTesterAdapter’, array( $adapteeDefinition )); ); } } }
  72. 72. where adapter is useful?
  73. 73. demo
  74. 74. backwards compatibility
  75. 75. backwards compatibility Backwards compatibility in Behat comes from the extensibility. 1. Everything is extension 2. New features are extensions too 3. New features could be toggled on/off
  76. 76. performance implications
  77. 77. performance implications · 2x more objects in v3 than in v2 · Value objects are used instead of simple types · A lot of additional concepts throughout · It must be slow
  78. 78. yet...
  79. 79. how?
  80. 80. how? immutability!
  81. 81. TestWork
  82. 82. TestWork
  83. 83. how?
  84. 84. Step1: Close the doors Assume you have no extension points by default. 1. Private properties 2. Final classes
  85. 85. Step 2: Open doors properly when you need them 1. Identify the need for extension points 2. Make extension points explicit
  86. 86. Private properties ...
  87. 87. Final classes
  88. 88. class BundleFeatureLocator extends FilesystemFeatureLocator { public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return parent::locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
  89. 89. final class BundleFeatureLocator implements SpecificationLocator { public function __construct(SpecificationLocator $baseLocator) { ... } public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return $this->baseLocator->locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
  90. 90. the most closed most extensible testing framework
  91. 91. ask questions close Feed! L♻♻ps: https://joind.in/11559

×