Dependency Injection Container (DIC) for Lazy Loading Services

Published: 5 April, 2012

Introduction

A lot of buzz is going on around Inversion of Control (IoC) patterns these days. IoC patterns separate object construction from business logic to create testable "seams" in your code (see Misko Hevery's Unit Testing Talk).

Unfortunately there is not yet a tried and tested methodology for IoC practices. Commonly used methods include Dependency Injection (DI) and Dependency Injection Container (DIC) / Service Locator (SL). This post summarizes some of the good an bad points for each method and proposes a way to use a class specific DIC to resolve some of the problems with generic DIC.

Using Dependency Injection (DI)

The standard way to do DI is to pass your dependencies into your class with either constructor arguments or setter methods. An external agent (the injector) is responsible for constructing the required objects and giving them to your object.

A good practice is to use constructor arguments for required dependencies and setters for optional dependencies.

The Good

  • Dependencies are explicit. This means programmers do not have to guess which dependencies are available in the class. Your IDE also knows which dependencies are available and can provide auto-complete.

The Bad

  • DI does not allow lazy loading of services. Some classes might require a dependency for only one method. Using DI means that all the dependencies must be instantiated and injected into your object, even if some are never used. A good example of this is a Controller object that requires a MailService object for just one action.
  • Classes with many dependencies have long constructors. Keeping track of the argument order becomes a nightmare when you have more than a few arguments.
  • Setters expose internal behavior. If you need to call a setter after creating an object for it to work properly, you are creating a leaky implementation - users of your object need to know how the object works to keep from breaking it. This creates a temporal coupling between the implementation and the consumer.

Dependency Injection Container (DIC)

The Good

  • Dependencies can be loaded on demand. DIC containers can instantiate a dependency (and all sub-dependencies) the first time a consumer asks for it. This is a major benefit of using a DIC over DI.

The Bad

  • Dependencies are hidden. Instead of your class depending on multiple other objects, it now depends on just the container object. This is bad in two ways: 1) programmers do not know dependencies except by looking at implementation, and 2) the IDE does not know dependencies and cannot provide auto-complete.
  • Class implementation must be aware of DIC. In order to take advantage of lazy loading of dependencies, the class must use the DIC internally to load services in the container. This might not be such a bad thing if you consider the DIC as a Service Loading Service, then just use it like any other service in your code.

Using Class Specific Dependency Injection Container (DIC) to Make Dependencies Explicit

Here propose a solution that to create a class specific DIC container when a DIC container is needed (such as when you want lazy loading for dependencies). The basic idea is to create a class specific DIC and inject it via the constructor. Any dependencies that do not require lazy loading should be injected into the constructor directly, all others should be managed by the DIC container.

For simplicity purposes, you could create your DIC as proxies to a application global DIC container so you can have one configuration for your whole application.

The following code demonstrates how to set it up. It is an example written in PHP using the Symfony2 Dependency Injection Components.

Example Data Importer with Complex Dependencies

    class Importer
    {
        private $serviceContainer;
        private $samples;

        /**
         * @param ImporterServiceContainer $serviceContainer
         */
        public function __construct($serviceContainer) {
            $this->serviceContainer = $serviceContainer;
            $this->samples = array();
        }

        /**
         * Imports an data from an experiment file, skipping data not allowed by filter.
         *
         * @param Filter $filter Filter for importing only desired data
         * @param string $filePath Experiment file to import
         */
        public function import($filter, $filePath) {
            // Process each combined row and build up samples
            $experimentFile = new ExperimentFile($filePath);
            $combinedRows = $experimentFile->getCombinedRows();
            foreach ($combinedRows as $combinedRow) {
                // Skip combined row if it doesn't pass filter criteria
                if ( ! $filter->isAllowed($combinedRows)) {
                    continue;
                }

                // Get citation data
                $pubmedId = $combinedRow->getPubmedId();
                $citationService = $this->serviceContainer->getCitationService();
                $citation = $strainService->findByPubmedId($pubmedId);

                // Get set strain data
                $strainName = $combinedRow->getStrainName();
                $strainService = $this->serviceContainer->getStrainService();
                $strain = $strainService->findByStrainName($strainName);

                // Build up a new sample
                $sample = new Sample();

                // Complex process for creating an new Sample object omitted for brevity
                // ...

                $sampleService = $this->serviceContainer->getSampleService();
                $sampleService->save($sample);

                // Store new sample
                $this->samples[] = $sample;
            }
        }
    }
    

Service Container Proxy

    use Symfony\Component\DependencyInjection\ContainerInterface;

    class ImporterServiceContainer
    {
        /**
         * @param ContainerInterface $container Service container to proxy
         */
        public function __construct($container) {
            $this->container = $container;
        }

        /**
         * @return CitationService
         */
        public function getCitationService() {
            return $this->container->get('citation_service');
        }

        /**
         * @return GeneService
         */
        public function getGeneService() {
            return $this->container->get('gene_service');
        }

        /**
         * @return SampleService
         */
        public function getSampleService() {
            return $this->container->get('sample_service');
        }
    }
    

Explanation of Code

We have a class that depends on many services, but we don't want to inject all dependencies into the constructor since there are cases where services may never need to be initialized (for example, if the filter skips all rows in the imported file).

To handle these cases, we need to inject a DIC to resolve these dependencies only when needed. Typically, injecting a global DIC would hide the dependencies our class needs, so we have created a class specific DIC to make these dependencies more explicit. In this case it is a proxy to a global ContainerInterface object, but you could also have made ImporterServiceContainer implement ContainerInterface directly.

Conclusions

A DIC is a powerful way to handle object dependency constructions, but can hide dependencies and couple your classes to DI frameworks. If you do not need the functionality DICs provide (such as lazy loading services), it is better to keep your objects separate from DI framework code with constructor or setter injection points.

When you need more dynamic construction, your should create a custom DIC that exposes only the services required by your class instead of using a generic one

Comments