Decoupling your (event) system

Posted on by Matthias Noback

About interface segregation, dependency inversion and package stability

You are creating a nice reusable package. Inside the package you want to use events to allow others to hook into your own code. You look at several event managers that are available. Since you are somewhat familiar with the Symfony EventDispatcher component already, you decide to add it to your package's composer.json:

{
    "name": "my/package"
    "require": {
        "symfony/event-dispatcher": "~2.5"
    }
}

Your dependency graph now looks like this:

Dependency graph

Introducing this dependency is not without any problem: everybody who uses my/package in their project will also pull in the symfony/event-dispatcher package, meaning they will now have yet another event dispatcher available in their project (a Laravel one, a Doctrine one, a Symfony one, etc.). This doesn't make sense, especially because event dispatchers all do (or can do) more or less the same thing.

Though this may be a minor inconvenience to most developers, having just this one extra dependency may cause some more serious problems when Composer tries to resolve version constraints. Maybe a project or one of its dependencies already has a dependency on symfony/event-dispatcher, but with version constraint >=2.3,<2.5...

Some drawbacks of the Symfony EventDispatcher

On top of these usability issues, when it comes to design principles, depending on a concrete library like the Symfony EventDispatcher is not such a good choice either. The EventDispatcherInterface which you will likely use in your code is quite bloated:

namespace Symfony\Component\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch($eventName, Event $event = null);

    public function addListener($eventName, $listener, $priority = 0);
    public function addSubscriber(EventSubscriberInterface $subscriber);
    public function removeListener($eventName, $listener);
    public function removeSubscriber(EventSubscriberInterface $subscriber);
    public function getListeners($eventName = null);
    public function hasListeners($eventName = null);
}

The interface basically violates the Interface Segregation Principle, which means that it serves too many different types of clients: most clients will only use the dispatch() method to actually dispatch an event, and other clients will only use addListener() and addSubscriber(). The remaining methods are probably not even used by any of the clients, or only by clients that help you to debug your application (a quick search in the Symfony code-base confirms this suspicion).

Personally, I think it's not really nice that events need to be objects extending the Event class. I understand why Symfony does it (it's basically because that class has the stopPropagation() and isPropagationStopped() methods which enables event listeners to stop the dispatcher from notifying the remaining listeners. I've never liked that, nor wanted it to happen in my code. Just like the original Observer pattern prescribes, all listeners (observers) should be able to respond to the current situation.

I also don't like the fact that the same event class can be used for different events (differentiated by just their name, which is the first argument of the dispatch() method). I prefer each type of event to have its own class. Therefore, to me it would make sense to pass just an event object to the dispatch() method, allowing it to return its own name, which will always be the same anyway. The name returned by the event itself can then be used to determine which listeners need to be notified.

According to these objections to the design of the Symfony EventDispatcher classes, we'd be better off with the following clean interfaces:

namespace My\Package;

interface Event
{
    public function getName();
}

interface EventDispatcher
{
    public function dispatch(Event $event);
}

It would be really great to be able to just use these two interface in my/package!

Symfony EventDispatcher: the good parts

Still, we also want to use the Symfony EventDispatcher. Above all we are familiar with it, but it also has some nice options, for example lazy-loading listeners, and when used in a Symfony application it offers an easy way to hook up listener services using service tags.

Introducing the Adapter

So we want to use our own event dispatching API, defined by the interfaces described above, but we also want to use the Symfony EventDispatcher which has a different public API. The solution is to create an adapter which bridges the gap between the two interfaces (following the famous Adapter pattern). In our case:

use My\Package\Event;
use My\Package\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class SymfonyEventDispatcher implements EventDispatcher
{
    private $eventDispatcher;

    public function __construct(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    public function dispatch(Event $event)
    {
        // call the relevant listeners registered to the Symfony event dispatchers

        ...
    }
}

It can be a bit tricky to follow what's going on because of all the names that are so much alike. But as you can see, a Symfony event dispatcher gets injected as a constructor argument into the adapter class. When the dispatch() method gets called, the adapter forwards the call to the Symfony event dispatcher.

Well, it's not very straight-forward to just forward the dispatch() call to the Symfony event dispatcher, since as we saw earlier that its method signature looks like this:

namespace Symfony\Component\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch($eventName, Event $event = null);
    ...
}

And unfortunately we don't have an object of type Symfony\Component\Event lying around, and we don't want one either, since that would totally reintroduce coupling to the Symfony component again.

Luckily the interface has another method we can use to bridge the gap: getListeners(), which means we can fetch the listeners ourselves and notify them manually:

class SymfonyEventDispatcher implements EventDispatcher
{
    ...

    public function dispatch(Event $event)
    {
        $listeners = $this->eventDispatcher->getListeners($event->getName());

        foreach ($listeners as $listener) {
            call_user_func($listener, $event);
        }
    }
}

We have now avoided the use of the Symfony Event class entirely. Inside our package we only need to use our own EventDispatcher interface and Event interface. This means we can remove the dependency on the symfony/event-dispatcher package altogether... almost!

The adapter class needs to be in its own package, to be able to fully decouple our package from symfony/event-dispatcher. Depending on what other (Symfony) components we are going to decouple from, we might call the adapter package my/package-symfony-bridge, or my/package-symfony-event-dispatcher, etc.

The resulting package dependency graph is quite beautiful:

New dependency graph

If you've read about package design principles before, you will know that this is a really nice constellation of packages because my/package does not depend on symfony/event-dispatcher anymore. In fact, it doesn't depend on anything anymore. We call such a package an independent package. At the same time, there are only packages depending on it, which makes my/package responsible. Being independent and responsible is what makes a package stable.

Also I'd like to point out that basically we have just applied an old and well-known design principle here: the Dependency inversion principle: we inverted the dependency direction. Instead of directly depending on a class (or in this case an interface) from another package, we defined our own interface. Then we created an adapter which enabled us to bridge the gap between our interface and the external interface.

Conclusion

There is one important conclusion to be mentioned here:

You don't need any dependencies at all.

In theory, your package would never need to depend on another package. Instead, it could define all kinds of interfaces itself and let adapter packages implement them. This would automatically make your package highly stable. It also makes it extremely reusable, because a user only needs to implement those interfaces specifically for the framework they use (if you haven't already done it for them!).

I'd like to mention the Aura packages here, which are designed in this way: no dependencies at all. It appears many people find this awkward and unnecessary, but given the approach described in this article, it should be more clear how such a thing is possible and even desirable.

Cover of Principles of PHP Package DesignIf you'd like to know more about this subject: I'm writing a book called Principles of PHP Package Design. You can buy it now and get all future updates (meaning: new chapters) for free.

PHP Symfony2 events package design coupling
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Maks Rafalko

I can't believe how many things and ideas from this post are similar to what I had during the development of a plugin for Tactician library.

Great post!

Matthias Noback

Thanks, that's good to hear!

Aleksandr Volochnev

Another great post. Thank you again!

roseffendi

I am struggling to fully decouple my domain layer to any external implementation. Fortunately i found this great article to help me decide is it necessary or not to have adapters.

Thanks, great post.

devosc

This is a great post Matthias. I would think another very common interface that would benefit from a consensual implementation is the Response object, e.g getHeaders, headers(), etc. I also found that the Request object doesn't even really need to exist since everything it contains can be retrieved from the Request object and directly injected via DI.

It may of helped to throw in the actual service method to demonstrate why it shouldn't matter (80% rule) which event system is being used, function update() { return $this->dispatch('Event\Name'); }

I actually found that for the Event Dispatcher all the other methods are better off being associated to a Configuration object instead, so that the Event Dispatcher could then just specify the 'dispatch' method, and if the Event Dispatch itself extends a Configuration, then access to the Event configuration can still be achieved via the Configuration interface, otherwise I would think the dispatcher may need to provide a 'getListeners' method in order to access the Configuration class.

I've found that there are two predominant ways of being able to configure a class object, either via its __construct method or by the class implementing a separate Configuration interface. This allows that service interface to be clearly separated and specifically for the SRP of that service; all the configuration calls happen via DI which does not need to know about that services public SRP API methods.

Would thinking about 'stop()' being the same as when a method returns null early? Not having that makes it harder to be a generalized.

On a side note; I think events classes should be able to control their own behaviour and can do so by having an __invoke method which the Dispatcher can detect and then allow the event class to call the listener (in which case the __invoke method is that event's own dispatcher), or the Dispatcher will call the listener and pass the event object as in your example. I called it the signal method.

Matthias Noback

Thanks for your comments. I myself am a great proponent of a separate Configuration interface. I've used it successfully to remove configuration methods. It also makes it possible to have different configuration loaders that should return an instance of the proposed interface.

I'm often using __invoke() too nowadays, because a method name often duplicates the name of the class, in fact just like EventDispatcher::dispatch(Event $event). With __invoke() you can just skip that part!

pbuyle

Great article. However, I'm not convinced by the solution to avoid implementing Symfony's Event interface for all event. If my listeners follows the documentation, they will expect the received event to be an instance of Symfony\Component\EventDispatcher\Event. With the suggested solution, they will not. Thus the solution break the contract given to the listeners. Another option is to provide an EventAdapter, to allow an
My\Package\Event to be consumed as a Symfony\Component\EventDispatcher\Event.

Matthias Noback

Thanks for bringing it up. I've been struggling with that question too. The thing is, there is no real problem, because the event listeners must not follow the Symfony documentation but your package's documentation. That is, they can and should type-hint to My\Package\Event or a more specific event class from the package. The user only leverages the Symfony component for its other advantages (service tags, lazy-loading of event listeners, etc.).

Paul M. Jones

I am a big fan of separated interfaces. Used them in Aura v1 all the time (as you note :-). Great article.

Julian Reyes E

Well during my reading, I came to the same conclusion as @weierophinney:disqus and I want to let you know the author that his view is not objective

Matthew Weier O'Phinney

Okay, big question: how do listeners attach to your event dispatcher interface implementation so that when dispatch() is called, they get notified? That part appears to be missing from your interface...

Another note: while an event system _can_ be used to implement the subject/observer pattern, that is only one use case. They can also be used to implement pub/sub systems (which are similar in scope to subject/observer), aspect-oriented programming (where AOP is not part of the language implementation)... and the Event Aggregator (http://martinfowler.com/eaa... or Event Notifier (http://www.marco.panizza.na... patterns. My feeling, in reading your post, is that you want subject/observer specifically -- and an event dispatcher may not be a good fit for your needs as it does _more_.

SplSubject/SplObserver _might_ be a good fit, but they require that the observers implement a specific interface (instead of being arbitrary PHP callables).

My point is: while you criticize event dispatcher solutions as being "bloated", you're judging them based on _your_ needs, not what they were designed for. If there's a mismatch, they may not be the right solution for _you_.

devosc

So for the ServiceLocatorInterface, how did the service get attached to the Service Manager. The point is that a subset of the larger interface needs to be grouped by itself. I find your statement a bit brash.

Matthias Noback

Thanks for your elaborate response! Some very good questions and criticisms. So there we go :)

"how do listeners attach to your event dispatcher"? My assumption is: it does not really matter for the way the event dispatcher is used in this package (though I realize I didn't mention this explicitly here!). There is no need to register listeners, just to offer a way to dispatch an event. This requires a RoleInterface (http://martinfowler.com/bli..., where one class has different faces depending on the client that uses it.

"My feeling, in reading your post, is that you want subject/observer
specifically -- and an event dispatcher may not be a good fit for your
needs as it does _more_." Excellent remark. And yes, I think this is true. It's also why there are so many event-managing packages out there. They can do the same thing, but some of them can do other things altogether. My point was that often you want to make use of some nice code in an existing package, but you don't want to rely on it to its full extent. You just want to talk to it through the smallest possible (i.e. client-specific) interface.

"while you criticize event dispatcher solutions as being "bloated",
you're judging them based on _your_ needs, not what they were designed
for. If there's a mismatch, they may not be the right solution for _you_": I feel that the attack portion ("violations", "bloated", etc.) of this post did not make my true intentions clear. What I wanted to express is that I wholeheartedly choose to use this package, but want to make only that part available that I really need, which means I have to make it conform to an interface I defined myself.

Matthew Weier O'Phinney

"I wholeheartedly choose to use this package, but want to make only that part available that I really need, which means I have to make it conform to an interface I defined myself." -- perfect response, Matthias! Now edit the article to say that, as I think that's the real point you're trying to get across. :)

Matthias Noback

:) I will try to make that more clear, thanks again.

cordoval

i think this package https://github.com/qandidat... supports better what @matthiasnoback:disqus is trying to say. I doubt he is attacking the symfony event dispatcher or any other. I think he has a very good point in terms of architecting one's applications and packages. Check the event dispatcher there, it clearly drives this point forward. no?

Goran

Don't you think that component creator should segregate component interface based on those use cases? Why should everyone deal with every use case inside his app?

Matthias Noback

This may also be very helpful. For instance a case earlier described is that of the service container, the ContainerInterface of which has methods for setting and getting services, though there are two, possibly mutually exclusive groups of clients using that method.

Matthew Weier O'Phinney

My point is that Matthias spent a good portion of the article attacking event systems... because they didn't suit his use case. I'm all for the separated interfaces, but feel that perhaps he should have chosen something that better suits the specific use case he has, and recognized that events are not the right solution.