decoupling with design patterns and symfony2 dic
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
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