In chapter 11 of the book "Clean Code: A Handbook of Agile Software Craftsmanship", Uncle Bob says the following Lazy-Initialization isn't a clean code. It takes two responsibilities and it has a hard dependency.
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Good enough default for most cases?
return service;
}
Besides IoC Container and Factory, are there any way to make that code clean and separated with the dependency?
What is going on with this example is that it violates both the Single Responsibility Principle and the Dependency Inversion Principle. Robert Martin already states just after the example:
Having both of these responsibilities means that the method is doing more than one thing, so we are breaking the Single Responsibility Principle.
He also touches the Dependency Inversion Principle by stating:
we now have a hard-coded dependency on
MyServiceImpl
and everything its constructor requires.
Having this hard-coded dependency means breaking the Dependency Inversion Principle.
The solution to this problem is not about using an IoC container or a factory. The solution here is to apply the Dependency Injection pattern and to:
have a global, consistent strategy for resolving our major dependencies.
If we apply the Dependency Injection pattern, our class will become much simpler and like like this:
public class Consumer
{
private Service service;
public Consumer(Service service) {
this.service = service;
}
public void SomeMethod() {
// use service
}
}
Note here that the Consumer
now doesn't expose the Service
anymore through its public methods. This shouldn't be needed, because a module should not share its internal state and if some other component needs to use our Service
, we can simply inject it into that other component directly.
The example above might seem to imply that we lost lazy initialization here, but that's not the case. We just moved the responsibility of lazy initialization to the "global, consistent strategy", a.k.a. the Composition Root.
Since Service
is an abstraction, we can create a proxy that just implements lazy initialization for our MyServiceImpl
(lazy initialization will be its single responsibility). Such proxy can look as follows:
internal class LazyServiceProxy : Service
{
// Here we make use of .NET's Lazy<T>. If your platform doesn't
// have this, such type is easily created.
private Lazy<Service> lazyService;
public LazyServiceProxy(Lazy<Service> lazyService) {
this.lazyService = lazyService;
}
public void ServiceMethod() {
// Lazy initialization happens here.
Service service = this.lazyService.Value;
service.ServiceMethod();
}
}
Here we created a LazyServiceProxy
and its sole purpose is to postpone the creation of the real service. It doesn't even require "a hard-coded dependency on MyServiceImpl
and everything its constructor requires".
In our composition root we can easily wire everything together as follows:
Service service = new LazyServiceProxy(
new Lazy<Service>(() => new MyServiceImpl(...)));
Consumer consumer = new Consumer(service);
Here we moved the responsibility of applying any lazy initialization to the start-up path of our application and we kept or Consumer
(and possibly many other components) clean from knowing anything about the Service
implementation being a heavyweights object. This even prevents us from letting our Consumer
from depending on a second ServiceFactory
abstraction.
Not only makes this extra factory abstraction the Consumer
more complex, it breaks the Dependency Inversion Principle in this specific case, because the fact that MyServiceImpl
is a heavyweight object, is an implementation detail and we are thus leaking implementation details through the factory abstraction. This goes against the Dependency Inversion Principle that states:
Abstractions should not depend on details.
As you can see, this solution does not require an IoC container (although you can still use one if you like) and doesn't require a factory. Although the factory design pattern is still valid when applying Dependency Injection, you will see that correctly apply SOLID and Dependency Injection will drastically reduce the need to use factories.