+ All Categories
Home > Software > Decoupling with Design Patterns and Symfony2 DIC

Decoupling with Design Patterns and Symfony2 DIC

Date post: 28-Nov-2014
Category:
Upload: konstantin-kudryashov
View: 1,183 times
Download: 3 times
Share this document with a friend
Description:
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.
97
Decoupling with Design Patterns and Symfony DIC
Transcript
Page 1: Decoupling with Design Patterns and Symfony2 DIC

Decoupling with Design Patterns

and Symfony DIC

Page 2: Decoupling with Design Patterns and Symfony2 DIC

@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

Page 3: Decoupling with Design Patterns and Symfony2 DIC
Page 4: Decoupling with Design Patterns and Symfony2 DIC
Page 5: Decoupling with Design Patterns and Symfony2 DIC
Page 6: Decoupling with Design Patterns and Symfony2 DIC

behat 3promise #1 (of 2):

extensibility

Page 7: Decoupling with Design Patterns and Symfony2 DIC

“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”

Page 8: Decoupling with Design Patterns and Symfony2 DIC

“So!ware entities (classes, modules, functions, etc.) should be open for

extension, but closed for modification”

Page 9: Decoupling with Design Patterns and Symfony2 DIC

behat 3promise #2 (of 2):

backwards compatibility

Page 10: Decoupling with Design Patterns and Symfony2 DIC
Page 11: Decoupling with Design Patterns and Symfony2 DIC

behat 3

- extensibility as the core concept

- BC through extensibility

Page 12: Decoupling with Design Patterns and Symfony2 DIC

Symfony BundlesBehat extensions

Page 13: Decoupling with Design Patterns and Symfony2 DIC

Symfony Bundles & Behat extensions1. Framework creates a temporary

container2. Framework asks the bundle to add its

services3. Framework merges all temporary

containers4. Framework compiles merged

container

Page 14: Decoupling with Design Patterns and Symfony2 DIC

interface CompilerPassInterface{ /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container * * @api */ public function process(ContainerBuilder $container);}

Page 15: Decoupling with Design Patterns and Symfony2 DIC

class YourSuperBundle extends Bundle{ public function build(ContainerBuilder $container) { parent::build($container);

$container->addCompilerPass(new YourCompilerPass()); }}

Page 16: Decoupling with Design Patterns and Symfony2 DIC

v3.0 v1.0(extensibility solution v1)

Page 17: Decoupling with Design Patterns and Symfony2 DIC

challenge:behat as the most extensible

test framework

Page 18: Decoupling with Design Patterns and Symfony2 DIC

pattern: observer

Page 19: Decoupling with Design Patterns and Symfony2 DIC

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

...}

Page 20: Decoupling with Design Patterns and Symfony2 DIC

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

...}

Page 21: Decoupling with Design Patterns and Symfony2 DIC

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

...}

Page 22: Decoupling with Design Patterns and Symfony2 DIC

extension point

Page 23: Decoupling with Design Patterns and Symfony2 DIC

<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="Symfony\Component\EventDispatcher\EventDispatcher"/>

<service id="hook.hook_dispatcher" class="Behat\Behat\Hook\EventSubscriber\HookDispatcher"> <argument type="service" id="event_dispatcher"/>

<tag name="event_subscriber"/> </service>

<service id="context.dictionary_reader" class="Behat\Behat\Context\EventSubscriber\DictionaryReader">

<tag name="event_subscriber"/> </service>

</services>

</container>

Page 24: Decoupling with Design Patterns and Symfony2 DIC

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

Page 25: Decoupling with Design Patterns and Symfony2 DIC

where event dispatcher / observer is useful?

Page 26: Decoupling with Design Patterns and Symfony2 DIC

pub/sub as an architectural choice

Page 27: Decoupling with Design Patterns and Symfony2 DIC

“Coupling is a degree to which each program module relies on each one of

the other modules”

Page 28: Decoupling with Design Patterns and Symfony2 DIC

“Cohesion is a degree to which the elements of a module belong together”

Page 29: Decoupling with Design Patterns and Symfony2 DIC

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

Page 30: Decoupling with Design Patterns and Symfony2 DIC

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

Page 31: Decoupling with Design Patterns and Symfony2 DIC

Coupling ↓Cohesion ↑

Page 32: Decoupling with Design Patterns and Symfony2 DIC

scratch that

Page 33: Decoupling with Design Patterns and Symfony2 DIC

v3.0 v2.0(extensibility solution v2)

Page 34: Decoupling with Design Patterns and Symfony2 DIC

There is no single solution for extensibility. Because extensibility is

not a single problem

Page 35: Decoupling with Design Patterns and Symfony2 DIC

framework extensionsSince v2.5 behat has some very

important extensions:1.MinkExtension

2. Symfony2Extension

Page 36: Decoupling with Design Patterns and Symfony2 DIC

problem:there are multiple possible

algorithms for a single responsibility

Page 37: Decoupling with Design Patterns and Symfony2 DIC

pattern: delegation loop

Page 38: Decoupling with Design Patterns and Symfony2 DIC

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) { ... } }}

Page 39: Decoupling with Design Patterns and Symfony2 DIC

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

Page 40: Decoupling with Design Patterns and Symfony2 DIC

extension point

Page 41: Decoupling with Design Patterns and Symfony2 DIC

<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="Behat\Testwork\Environment\EnvironmentManager” />

<service id=“behat.context.environment.handler” class=“Behat\Behat\Context\Environment\ContextEnvironmentHandler”>

<tag name=“environment.handler”/> </service>

</services>

</container>

Page 42: Decoupling with Design Patterns and Symfony2 DIC

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

Page 43: Decoupling with Design Patterns and Symfony2 DIC

where delegation loop is useful?

Page 44: Decoupling with Design Patterns and Symfony2 DIC

behat testersThere are 5 testers in behat core:

1. FeatureTester2. ScenarioTester3. OutlineTester

4. BackgroundTester5. StepTester

Page 45: Decoupling with Design Patterns and Symfony2 DIC

behat testersBehat needs to provide you with:

· Hooks· Events

Page 46: Decoupling with Design Patterns and Symfony2 DIC

problem:we need to dynamically extend

the core testers behaviour

Page 47: Decoupling with Design Patterns and Symfony2 DIC

pattern: decorator

Page 48: Decoupling with Design Patterns and Symfony2 DIC

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

Page 49: Decoupling with Design Patterns and Symfony2 DIC

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

Page 50: Decoupling with Design Patterns and Symfony2 DIC

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

Page 51: Decoupling with Design Patterns and Symfony2 DIC

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

Page 52: Decoupling with Design Patterns and Symfony2 DIC

extension point

Page 53: Decoupling with Design Patterns and Symfony2 DIC

<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="Behat\Behat\Tester\ScenarioTester” />

<service id=“hooks.tester.scenario” class=“Behat\Behat\Hooks\Tester\ScenarioTester”> ...

<tag name=“tester.scenario_wrapper” order=“100”/> </service>

<service id=“events.tester.scenario” class=“Behat\Behat\Events\Tester\ScenarioTester”> ...

<tag name=“tester.scenario_wrapper” order=“200”/> </service>

</services>

</container>

Page 54: Decoupling with Design Patterns and Symfony2 DIC

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 Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass

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

...}

Page 55: Decoupling with Design Patterns and Symfony2 DIC

where decorator is useful?

Page 56: Decoupling with Design Patterns and Symfony2 DIC

behat outputBehat has a very simple output:

Page 57: Decoupling with Design Patterns and Symfony2 DIC

behat outputUntil you start using backgrounds:

Page 58: Decoupling with Design Patterns and Symfony2 DIC

behat outputAnd throwing exceptions from their

hooks:

Page 59: Decoupling with Design Patterns and Symfony2 DIC

problem:we need to add behaviour to

complex output logic

Page 60: Decoupling with Design Patterns and Symfony2 DIC

pattern: observer

Page 61: Decoupling with Design Patterns and Symfony2 DIC

pattern: chain of responsibility

Page 62: Decoupling with Design Patterns and Symfony2 DIC

pattern: composite

Page 63: Decoupling with Design Patterns and Symfony2 DIC

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

Page 64: Decoupling with Design Patterns and Symfony2 DIC

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

...}

Page 65: Decoupling with Design Patterns and Symfony2 DIC

Event listenersBehat has 2 types of listeners:

1. Printers2. Flow controllers

Page 66: Decoupling with Design Patterns and Symfony2 DIC

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

...}

Page 67: Decoupling with Design Patterns and Symfony2 DIC

How do backgrounds work?

Page 68: Decoupling with Design Patterns and Symfony2 DIC

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

Page 69: Decoupling with Design Patterns and Symfony2 DIC

where composite and CoR are useful?

Page 70: Decoupling with Design Patterns and Symfony2 DIC
Page 71: Decoupling with Design Patterns and Symfony2 DIC

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

Page 72: Decoupling with Design Patterns and Symfony2 DIC

problem:we need to introduce

backwards incompatible change into the API

Page 73: Decoupling with Design Patterns and Symfony2 DIC

pattern: adapter

Page 74: Decoupling with Design Patterns and Symfony2 DIC

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

Page 75: Decoupling with Design Patterns and Symfony2 DIC

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

Page 76: Decoupling with Design Patterns and Symfony2 DIC

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

Page 77: Decoupling with Design Patterns and Symfony2 DIC

where adapter is useful?

Page 78: Decoupling with Design Patterns and Symfony2 DIC

demo

Page 79: Decoupling with Design Patterns and Symfony2 DIC

backwards compatibility

Page 80: Decoupling with Design Patterns and Symfony2 DIC

backwards compatibilityBackwards compatibility in Behat

comes from the extensibility.1. Everything is extension

2. New features are extensions too3. New features could be toggled on/off

Page 81: Decoupling with Design Patterns and Symfony2 DIC

performance implications

Page 82: Decoupling with Design Patterns and Symfony2 DIC

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

Page 83: Decoupling with Design Patterns and Symfony2 DIC

yet...

Page 84: Decoupling with Design Patterns and Symfony2 DIC
Page 85: Decoupling with Design Patterns and Symfony2 DIC

how?

Page 86: Decoupling with Design Patterns and Symfony2 DIC

how?

immutability!

Page 87: Decoupling with Design Patterns and Symfony2 DIC

TestWork

Page 88: Decoupling with Design Patterns and Symfony2 DIC

TestWork

Page 89: Decoupling with Design Patterns and Symfony2 DIC

how?

Page 90: Decoupling with Design Patterns and Symfony2 DIC

Step1: Close the doorsAssume you have no extension points

by default.1. Private properties

2. Final classes

Page 91: Decoupling with Design Patterns and Symfony2 DIC

Step 2: Open doors properly when you need them

1. Identify the need for extension points2. Make extension points explicit

Page 92: Decoupling with Design Patterns and Symfony2 DIC

Private properties

...

Page 93: Decoupling with Design Patterns and Symfony2 DIC

Final classes

Page 94: Decoupling with Design Patterns and Symfony2 DIC

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

Page 95: Decoupling with Design Patterns and Symfony2 DIC

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

Page 96: Decoupling with Design Patterns and Symfony2 DIC

the most closed most extensible testing

framework

Page 97: Decoupling with Design Patterns and Symfony2 DIC

ask questionsclose Feed! L♻♻ps:https://joind.in/11559


Recommended