Decoupling with Design Patterns and Symfony2 DIC

Post on 28-Nov-2014

1.198 views 3 download

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.

Transcript of Decoupling with Design Patterns and Symfony2 DIC

Decoupling with Design Patterns

and Symfony 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

behat 3promise #1 (of 2):

extensibility

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

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

extension, but closed for modification”

behat 3promise #2 (of 2):

backwards compatibility

behat 3

- extensibility as the core concept

- BC through extensibility

Symfony BundlesBehat extensions

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

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

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

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

v3.0 v1.0(extensibility solution v1)

challenge:behat as the most extensible

test framework

pattern: observer

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

...}

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

...}

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

...}

extension point

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

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

where event dispatcher / observer is useful?

pub/sub as an architectural choice

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

the other modules”

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

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

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

Coupling ↓Cohesion ↑

scratch that

v3.0 v2.0(extensibility solution v2)

There is no single solution for extensibility. Because extensibility is

not a single problem

framework extensionsSince v2.5 behat has some very

important extensions:1.MinkExtension

2. Symfony2Extension

problem:there are multiple possible

algorithms for a single responsibility

pattern: delegation loop

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

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

extension point

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

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

where delegation loop is useful?

behat testersThere are 5 testers in behat core:

1. FeatureTester2. ScenarioTester3. OutlineTester

4. BackgroundTester5. StepTester

behat testersBehat needs to provide you with:

· Hooks· Events

problem:we need to dynamically extend

the core testers behaviour

pattern: decorator

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

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

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

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

extension point

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

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

...}

where decorator is useful?

behat outputBehat has a very simple output:

behat outputUntil you start using backgrounds:

behat outputAnd throwing exceptions from their

hooks:

problem:we need to add behaviour to

complex output logic

pattern: observer

pattern: chain of responsibility

pattern: composite

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

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

...}

Event listenersBehat has 2 types of listeners:

1. Printers2. Flow controllers

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

...}

How do backgrounds work?

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

where composite and CoR are useful?

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

problem:we need to introduce

backwards incompatible change into the API

pattern: adapter

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

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

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

where adapter is useful?

demo

backwards compatibility

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

performance implications

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

yet...

how?

how?

immutability!

TestWork

TestWork

how?

Step1: Close the doorsAssume you have no extension points

by default.1. Private properties

2. Final classes

Step 2: Open doors properly when you need them

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

Private properties

...

Final classes

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

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

the most closed most extensible testing

framework

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