DI versus Lazy loading
Lazy loading is a design pattern that delays the creation of objects until the application actually needs them. How to reconcile this with Dependency Injection, which in turn likes to get objects already in the constructors?
As we have discussed, Dependency Injection is obviously dependency passing, that is, each class openly claims its dependencies in initialization methods, usually directly in the constructor.
Let's have a SignPresenter that displays and manages a form for logging into
an application. When the form is submitted, a method
formSubmitted()
is called to authenticate the login credentials
using an authenticator. Simplified code would look like this:
class SignPresenter
{
function formSubmitted()
{
// $GLOBALS['authenticator']->authenticate(...)
Registry::getAuthenticator->authenticate(...)
}
}
In keeping with the DI principle, we will not enchant the authenticator from the global environment, but will admit this dependency in the constructor:
class SignPresenter
{
private $auth;
function __construct(Authenticator $auth)
{
$this->auth = $auth;
}
function formSubmitted()
{
$this->auth->authenticate(...);
}
}
In practice, however, we will run into a fundamental problem: there will be 1000 form views per form submission. However, the authenticator will always initialize. And since it authenticates against the database, it will, in accordance with DI, require a PDO object in the constructor, the creation of which will cause it to connect to the database server.
Thus, every form view will be accompanied by loading classes 99.9% of the time that are unnecessary, creating unnecessary objects and making unnecessary connections to the database.
This is a serious shortcoming that lazy loading will solve. One option is to
create a so-called proxy, an object that implements the interface
Authenticator
and wraps the original authenticator, but
instantiates it only when a method is called authenticate()
. The
other option, which requires a change to the presenter, is to pass a factory
instead of the authenticator, which will produce it later:
class SignPresenter
{
private $authFactory;
private $auth;
function __construct(AuthenticatorFactory $authFactory)
{
$this->authFactory = $authFactory;
}
function getAuthenticator()
{
if (!$this->auth) {
$this->auth = $this->authFactory->create();
}
return $this->auth;
}
function formSubmitted()
{
$this->getAuthenticator()->authenticate(...);
}
}
interface AuthenticatorFactory
{
/** @return Authenticator */
function create();
}
Method getAuthenticator()
ensures that we work with a single
authenticator instance in the SignPresenter class.
And this is where the article could end. But it doesn't.
Don't take the factory!
Did using a factory seem like a good idea or a no-brainer to you? Then slow down for a moment.
Try to think about the difference between:
- I'll need to get the object
- I need to make an object
We will need to get the object, which is a more general formulation than make.
Let's go back to the first DI example, where we're getting an authenticator via a constructor. What does the constructor header say? That the authenticator is to be passed to it. Not that it is to be made and then passed. The method that creates the SignPresenter instance can get the authenticator any way it wants (and maybe produce it), but the presenter itself doesn't give a shit. It just requests it and doesn't ask for the origin.
But the factory solution, in addition to supporting lazy loading, also anticipates the way the object is obtained: it will be produced. So while in the first case the SignPresenter gets one authenticator, in the second case it gets a tool to make multiple authenticators. But that wasn't our point. We're not facing the need to produce authenticators, we're addressing the need to lazy-load a single authenticator.
The seemingly correct deployment of the factory is flawed, I'll come back to that in a moment. The correct solution is to hand over something that the authenticator will later return to us (not necessarily produce) instead of the factory. Let's call it a Getter or an Accessor, for example (it is in no way a Service locator):
class SignPresenter
{
private $authAccessor;
function __construct(AuthenticatorAccessor $authAccessor)
{
$this->authAccessor = $authAccessor;
}
function formSubmitted()
{
$this->authAccessor->get()->authenticate(...);
}
}
interface AuthenticatorAccessor
{
/** @return Authenticator */
function get();
}
The presenter code is also simplified because we don't need the
getAuthenticator()
method. The accessor itself ensures that we are
still working with the same instance.
I list both AuthenticatorFactory
and
AuthenticatorAccessor
as interfaces because the implementation
doesn't matter at all.
Let's try to see what testing a presenter might look like in practice, for example:
// vyrobíme si mockovaný autentikátor
$auth = ...;
// a potřebujeme ho dostat do presenteru
$presenter = new SignPresenter(new TrivialAuthenticatorAccessor($auth));
where TrivialAuthenticatorAccessor
is indeed trivial:
class TrivialAuthenticatorAccessor implements AuthenticatorAccessor
{
private $instance;
function __construct(Authenticator $instance)
{
$this->instance = $instance;
}
function get()
{
return $this->instance;
}
}
If instead of an accessor we went the originally proposed factory route, we
would have quite a problem how to sneak $auth
into the presenter.
(An example of how testing leads to better code design, by the way).
Any factory can be easily transformed into an accessor, for example by using
the generic CallbackAccessor
:
abstract class CallbackAccessor
{
private $instance;
private $callback;
function __construct(/*callable*/ $callback)
{
$this->callback = $callback;
}
function get()
{
if (!$this->instance) {
$this->instance = call_user_func($this->callback);
}
return $this->instance;
}
}
Any callback can then be transformed into the AuthenticatorAccessor form:
class CallbackAuthenticatorAccessor extends CallbackAccessor implements AuthenticatorAccessor
{}
$presenter = new SignPresenter(new CallbackAuthenticatorAccessor(function(){
return ...;
}));
All parts:
- What is Dependency Injection?
- Dependency Injection versus Service Locator
- Dependency Injection and passing of dependencies
- Dependency Injection and property injection
- Dependency Injection versus Lazy loading (you are currently reading)
Sign in to submit a comment