Decoupling packages is a hard thing. There are not a lot of options, and this blog post is about how some options are better than others.
Let’s say for example that you are writing a “package”, or library, to respond to HTTP requests (that kind of package could be considered the basis for a web framework). How do you handle routing?
If you write your Router package as an independent package (which is good: small and specialized packages are more reusable and maintainable), you might not want to couple the HTTP package to the Router package: you want to leave users free to choose the router of their choice.
So, what are your options to make the HTTP package and the Router package decoupled from each other?
The first option is to turn to event-driven programming. By relying on an “event manager” library/package, you can have your package raise specific events at strategic points in your code. And you can have those events affect the code flow.
This is the solution chosen by Symfony to solve the routing problem exposed earlier. Their HttpKernel component is decoupled form their Routing component through events.
Here is the simplified code flow of their
HttpKernel class/component (which is the basis for an HTTP application):
1 2 3 4 5 6 7 8 9 10
So here is what’s happening:
HttpKernelraises a “Kernel Request” event
- then it expects that a listener set the controller under the “_controller” key in the request
HttpKernel decoupled from the
Router component? Yes. The listener that sets the controller could actually be anything.
The problem is, as emphasized above, that the
HttpKernel expects something very specific from the listeners. The whole application depends on an unknown listener being actually registered for that particular event and following an exact, unspecified behavior.
I believe events should be used for hooking up in the main logic flow to extend it. But the main logic flow should be linear and not rely on the possible side effects of an event.
There are some other problems we can find with this solution:
- the package ends up coupled to the “event manager” package (you just replaced a dependency by another)
- the code is not linear: it makes it much harder for developers to put the pieces back together and contribute to the project
- behavior from decoupled packages are not specified by contracts
Regarding the pros:
- the behavior is not specified, which can also be a good thing: it leaves every possible option open for the future
- might be simpler than the other options
<disclaimer>Just to be clear, Symfony is an exceptional framework and I love it. I am sure the decision to choose this option was carefully thought through, and the important thing to remember is that it works. Symfony is probably the most popular modern PHP framework, and my blog post doesn’t change that. I am just using it as an example here, I just want to expose alternative options and discuss the pros and cons.
Interfaces and adapters
I was mentioning specifying behaviors with contracts. In PHP (and most OO languages), you can implement this using interfaces.
If we take the previous example, an HTTP package could contain a
RouterInterface to specify how a routing component should behave (this is a very basic example):
1 2 3 4 5 6 7 8
You’ll notice that not everything is specifiable, e.g. the return types. Hopefully PHP will allow that in its next major version, but until then the only solution is to use documentation.
HttpApplication class, we can use it in type-hints to accept any implementation (classic dependency injection here). And here is what the code logic would look like:
1 2 3 4 5
The code flow is linear and completely explicit. And the HTTP package is decoupled from any Router package!
The only problem left: Router packages would be forced to implement
Acme\Http\RouterInterface if they want to be used with the HTTP package. Because of that, they end up coupled to it… So how do we decouple Router packages from the HTTP package?
A way to go around that is to use adapters:
1 2 3 4 5 6 7 8 9 10 11 12 13
Thanks to that adapter, the
Router class doesn’t need to implement the
RouterInterface. So it is completely decoupled from the HTTP package.
However, as you can see, it requires writing an adapter each time you want to “connect” decoupled packages.
- linear and explicit code flow
- specified behavior (using interfaces)
- requires to write interfaces
- requires to write adapters
This strategy of interfaces was recently used by Laravel. For the version 5.0 (IIRC), Laravel will publish a package named
illuminate/contracts which contains all the interfaces used by its other packages.
That allows to have decoupled Laravel packages while not needing adapters to use them together: packages can implement the interfaces at the very small cost of being coupled to
illuminate/contracts (it’s a small cost because the package is very light and contains only interfaces).
Now the last option is to go a step beyond and try to make the interfaces “standard”. By that I mean that the same interface would be used by many packages, and implemented by many others.
Today, many logging packages implement the
Psr\Log\LoggerInterface, and most modern frameworks type-hint against that interface instead of specific implementations. That means that users can choose any PSR-3 compliant logger and have their framework use it.
Needless to say that this is an ideal situation: no coupling, no effort! But logging was kind of an easy topic. It’s very hard to come up with standards for all the other components that need interfaces, mainly because implementations often differ a lot.
The PHP-FIG has been working for a few years on a Cache and an HTTP message PSR, and hopefully they will be released sometime. In the meantime, the container-interop project aims at providing interfaces to standardize the usage of dependency injection containers.
OK, there’s not much left to add here, if you have any reaction about this I’d be happy to hear it. If I got anything wrong, I’d be happy to correct it.
I would like to finish on an idea that was suggested about a year ago on the PHP internals mailing list: “weak interfaces”. Those are interfaces that define a behavior, but that do not need to be implemented by classes. It mixes the principle of static-typing with duck-typing:
If it looks like a duck and quacks like a duck, then it’s a duck.
What’s really good with this is that it allows packages to define their interfaces, and type-hint against it, all the while not requiring dependencies to actually implement it. As long as objects are compatible with the interface, it works. It’s a sort of
class X implements Y evaluated at runtime. Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
This example was just for fun, but I wish such a feature would land in PHP (along with static return type). It would help a lot with package interoperability and decoupling.
Update: the original RFC about what I called weak interfaces has been linked to me by its author, Anthony Ferrara. Here it is: PHP RFC: Structural Type Hinting. As you can see, it was named Structural type hinting instead of weak interface.
It has also been pointed to me in the comments that the same Anthony Ferrara wrote a userland implementation of this: ProtocolLib.