Some have heard that singletons are evil. They are. Don't use them.
TL;DR And don't use facades either, if you don't have to.
Towards better tomorrow
"But singletons are so easy to use!" 👴
// Singleton
class SomeService
{
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new static();
}
return self::$instance;
}
}
It is also simple to retain the utility of singletons and still use dependency injection, to a degree.
It's not perfect, but it's a nice compromise for the imperfect world.
There is only one difference between a singleton and a facade, and it's the explicit binding of the instance:
An explicit binding call must occur prior to using a facade.
// Facade
class SomeService
{
public static function getInstance(): self {
if (self::$instance === null) {
throw new LogicException();
}
return self::$instance;
}
public static function bind(?self $instance): void
{
self::$instance = $instance;
}
}
The binding is usually done during the bootstrap phase of an application, using the app's service container.
// Bootstrap the service container somehow...
$container = $bootstrap->container();
// Then bind the facade
SomeService::bind($container->get(SomeService::class));
Now both the injection via the service container will work as well as using the facade directly.
This way, a legacy app may be refactored so that the legacy code remains functional, but dependency injection is introduced to the newer parts.
An example of using an interface
When implementing a facade, interfaces should always be used for the instances being bound. This will allow swapping the implementations behind them.
final class EventBus
{
private static ?EventDispatcherInterface $instance = null;
public static function fire(EventInterface $event): void
{
if (self::$instance === null) {
throw new LogicException('The facade has not been bound to an instance.');
}
self::$instance->dispatch($event);
}
public static function bind(?EventDispatcherInterface $instance, bool $force = false): void
{
if (!$force && self::$instance !== null) {
throw new LogicException(...);
}
self::$instance = $instance;
}
}
Configure it during app's startup
$container = ...;
EventBus::bind($container->get(EventDispatcherInterface::class));
Now, both DI and the facade can coexist:
$event = new SomeEvent(...);
$dispatcher = $container->get(EventDispatcherInterface::class);
$dispatcher->dispatch($event);
EventBus::fire($event);
You may ask why the $force
parameter of the EventBus::bind
method. For practical purposes: You will want to test your facades and to do so, rebind them.
I also suggest throwing an explanatory exception when attempting to bind the facade that has already been bound, like this one:
throw new LogicException(sprintf(
'The facade has already been bound.' . ' ' .
'This error might imply design flaws.' . ' ' .
'Consider injecting an %s implementation instead of using a facade.',
EventDispatcherInterface::class,
));
Are singletons the root of all evil?
Dont' get the wrong idea. You should NOT use facades just to allow global access to services. You still want to employ dependency injection and inversion of control (IoC) principles.
Facades build upon the very same bad idea of global access that drives singletons.
On the other hand, there are cases where they might be practical (e.g. logging).
And then there are legacy apps that come with singletons already in place.
Or legacy apps from the dark age that use no means of dependency injection at all.
Implementing facades and utilizing a DI container might be a great step forward in those situations.
What is wrong with singletons then?
Tight couplings. Well, all static calls and new
operator uses create tight couplings.
Tightly coupled code is harder to extend and harder to maintain, because implementations that have gotten deprecated can not be swapped for new ones. Tightly coupled code is harder to test, because big parts of the code needs to be mocked.
When working with a facade that binds to an instance through an interface, the implementation behind the facade is allowed to change without modifications to the calling code.
This alleviates the problem somewhat.
Why do singletons even exist?
The singleton pattern solved an even greater evil of the dark ages - global variables. Singletons at least are immutable, while global variables may change on a whim.
Singletons are a part of programming history and as such deserve respect. There is no place for them in a modern code, however.
Takeaway
Facades are not the clean solution that makes us happy and comfortable at the same time. But they are a better option than singletons.
Especially in legacy apps where they provide compatibility with existing code while also paving a path towards brighter future.
Top comments (0)