Importance of Dependency Injection in Web Development

Importance of Dependency Injection in Web Development

  • 293

Importance of Dependency Injection in Web Development .Dependency Injection (DI) software design pattern has long been part of native client and server-side applications that use OOP languages

Dependency Injection (DI) software design pattern has long been part of native client and server-side applications that use OOP languages. In essence it’s a technique for achieving Inversion of Control (IoC) between classes and their dependencies. With the rise of enterprise focused front-end frameworks like Angular and Ember, many web developers have become familiar with the pattern and mechanism of DI container.

Since most of our technical team have enterprise Java background, we have repeatedly seen the benefits of adding DI into application’s architecture, so it’s no wonder we brought it to the ag-Grid stack. Interestingly, DI is somewhat intangible and confusing topic which is exactly what makes it complex. In fact, there’s an entire book devoted to this single pattern and the mechanism known as DI (IoC) Container.

However, if you read the book you’ll find most of the information only slightly relevant to web development. The content mostly discusses cases related to architecture of native applications that use OOP languages like Java or C#. If you want to know the reasons for using DI in web applications, there’s almost no information available. Most of the explanations you’d find would be about how DI makes unit-tests easier. Yet, it’s important to realize that the purpose of DI is much broader than just enabling unit testing.

In this article, I want to shed some light onto the need for having DI in web applications by showing you why and how we introduced this mechanism into ag-Grid stack. I’ll wrap the article up by giving you an architectural overview of the DI Container which we implemented ourselves inside the grid.

Let’s get started.

This blog post is a continuation of the series where we take a look at patterns and techniques we use inside ag-Grid. If you haven’t read my previous post Inside ag-Grid: techniques to make the fastest JavaScript datagrid in the world, I also recommend that you read it.

The need for Dependency Injection in web applications and ag-Grid

I’ve read many articles about DI and most of them define two major benefits of using DI: enabling loose coupling of code (decoupling) and simplifying testing setup. While making testing easier, particular enabling stubbing, is easy to grasp if you just try it once, explaining how DI enables loose coupling requires a bit of theory and is less tangible.

Requirements constantly change which implies continuous refactoring and modifying our code on almost everyday basis. Decoupling, which basically means writing loosely coupled code, is a powerful tool to help make those changes easier. But what is loosed coupled code? Well, when two blocks of code are loosely coupled, it means that a change in one usually doesn’t require a change in the other. The fewer places in code you have to touch to make a change, the easier it is to maintain the code. So the biggest advantage of loose coupling is that it increases maintainability of the overall solution.

One way to achieve loose coupling in OOP languages like Java or C# is to program to an interface instead of a concrete implementation. That’s where DI really helps. Here’s the quote from this great article about DI containers:

The assumption DI containers are making is, that you do have abstractions. Getting a little bit ahead of ourselves, one of the main reason for using the container is to maintain lose coupling in our code. The way we achieve this is by constructing our types in such a way that they do not depend directly on other concrete classes. Regardless if it’s a high level type, or low level type, instead of depending on one another directly, container expects they will depend on abstractions.

But we don’t really have interfaces in web applications. All classes are concrete in JavaScript. So does DI still enable loose coupling in web applications? Here at ag-Grid we believe the answer is “yes”.

You’ve probably heard about The Law of Demeter (Lod) also known as the principle of least knowledge. In its general form, the LoD is a specific case of loose coupling. Adhering to this law allows you achieve loose coupling by ensuring that an object knows as little as possible about the structure or properties of anything else. That knowledge includes the details of construction of an object that another object depends on. However, without Dependency Injection the opposite happens. The knowledge about to how find a dependency and how to initialize it spreads across your application.

Here’s an example. Inside ag-Grid we have a **RowRenderer** service that depends on **ColumnController**, **GridApi**and **EventService**. Naturally, we somehow need to obtain references to these services inside the **RowRenderer** class. Here’s how the code would look without using DI for **RowRenderer** service:

RowRenderer.ts

class RowRenderer {
    private columnController: ColumnController;
    private gridApi: GridApi;
    private eventService: EventService;

    constructor() {
        this.columnController = new ColumnController();
        this.gridApi = new GridApi();
        this.eventService = new EventService();
    }
}

Here we simply instantiate all objects that **RowRenderer** needs inside the constructor. There are a couple problems with such approach. First, the created instances become private inside the class. If some other class would need them it would have to create their own instances. This leads to duplication of code and redundant memory consumption. Second, it won’t work this way if any of the services need to be singletons. Third, dynamically mocking services that are created inside the class during testing is pretty cumbersome to setup. And finally, **RowRenderer** service knows too much about the way objects are constructed. If you change the number or order of parameters to any of the services **RowRenderer** depends on, you’ll need to update code in all places where these services are used. This way of wiring dependencies together goes against the Law of Demeter and is tightly coupled. However, this is the best approach you can get if you’re not using Dependency Injection pattern.

Knowing that here at ag-Grid we started to use the DI pattern right from the start. In the beginning, we were using the implementation of the pattern known as Pure DI where all components are created explicitly by the application code. The application code defines how to create the components and wire dependencies, no external tools or specialized wiring modules of code are used for that purpose.

This is how the **RowRenderer** got its dependencies in the first versions of ag-Grid. All classes had the **init** method through which the dependencies would be passed to the service:

RowRenderer.ts

class RowRenderer {
    private columnController: ColumnController;
    private gridApi: GridApi;
    private eventService: EventService;

    public init(columnController, gridApi, eventService) {
        this.columnController = columnController;
        this.gridApi = gridApi;
        this.eventService = eventService;
    }
}

There was also a function called **setupComponents** responsible for instantiating and wiring the dependencies together:

Grid.ts

export class Grid {
    private setupComponents(...) {

        // create all the services
        var columnController = new ColumnController();
        var gridApi = new GridApi();
        var eventService = new EventService();


        // intialize the service by passing the dependencies
        rowRenderer.init(
            ...,
            columnController,
            gridApi,
            eventService,
            ...
        );
    }
}

This function could be referred to as Composition Root, which is a single point in an application where the entire object graph is composed at once. Since the Composition Root composes the entire object graph, it has access to the entire context, which enables it to make intelligent decisions about which dependencies go where.

This way of linking dependencies together solved most of the problems outlined in the previous section. However, if you explore the source code of the [**setupComponents**](https://github.com/ag-grid/ag-grid/blob/3.3.0/src/ts/grid.ts#L180) function you’ll see how much stuff there is inside which makes the function 140 lines long. That’s a lot of code. You don’t have to read and understand all of this code to appreciate that it might require a bit of maintenance. There’s a big object graph hidden in the code, with some shared sub-graphs, and since it uses the new keyword to create all the objects, every time you change a constructor signature, you’ll need to update this code, otherwise it won’t compile.

On top of that, the dependencies chain can become nested, which quickly makes it unwieldy to wire the services up manually. Check out this code as an example:

example.ts

var svc = new ShippingService(new ProductLocator(),
    new PricingService(), new InventoryService(),
    new TrackingRepository(new ConfigProvider()),
    new Logger(new EmailLogger(new ConfigProvider())));

So, after version 3 it was clear that we needed a standalone module specialized in wiring the dependencies together to solve all of those problems mentioned above. This kind of functionality is known as Dependency Injection Container.

Before we get to the Container, I’d like to say a few words about how Dependency Injection makes testing process easier. As you know, testing process is all about instantiating small portions of your application, running objects through various scenarios and asserting some output. And if this instantiation part is difficult, it’s very hard to test things.

For example, in order to test a class, you need to create an instance of the class. Which means that the constructor of the class would get called. Ideally, to make testing setup easier, we’d want to put only assignments into the constructor. However without DI, you’ll have a lot of logic in constructors that deal with object lookup and construction. DI eliminates the unnecessary work in the constructor and makes code a lot easier to test. And if you’re using a standalone DI container to take care of dependencies, your only concern is tests for business logic, while wiring should work correctly because of the container.

Introducing DI container into ag-Grid

And now, finally, we’re ready to talk about the concept of DI Containers. As I demonstrated above, with Pure Dependency Injection all components are created explicitly by an application code. There’s no specialized module responsible for wiring the dependencies together. But Pure DI is rare. Most applications and frameworks include a helper module referred to as DI Container that helps automate the wiring and instantiation. Angular and Ember have DI Containers.

Using a DI Container to compose object graphs by convention presents an unparalleled opportunity to push infrastructure code to the background.

The container knows how to create all of your objects and their dependencies, so you can easily get an object with all of its dependencies with one simple call. It automatically creates an object of the specified class and also injects all the dependency objects through a constructor at run time and disposes it at the appropriate time. This is done so that we don’t have to create, maintain and also manage objects manually. Because doing it right is hard.

Each service might be slightly different. For some of them we need to have just a single instance, that is reused everywhere, for others, we want to provide a new instance each time one is needed, or may be reuse instances within the scope of a single cell, or any other arbitrary scope. To satisfy the above-mentioned requirements and maintain loose coupling we needed to write a lot of code that grew both in size and complexity with every change.

DI container was meant to replace that code. Because ag-Grid’s philosophy is to have no dependencies, we decided to write our own implementation of the container. We modeled it after the IoC container in Spring framework. Because TypeScript that we use for development supports decorators (annotations) we could carry over some design decisions from Spring. Particularly, we the use the **@Bean** decorator to denote a service that should be managed by the DI container and **@Autowired** to identify dependencies that should be injected by the container.

This is how the code looks today:

RowRenderer.ts

@Bean("rowRenderer")
export class RowRenderer extends BeanStub {
    @Autowired("columnController") private columnController: ColumnController;
    @Autowired("gridApi") private gridApi: GridApi;
    @Autowired("eventService") private eventService: EventService;
}

There’s no assignments inside the service, it’s all done by the container. In previous implementations we also had a function called **setupComponents** responsible for instantiating and wiring the dependencies together. In the current version the code migrated inside the DI container implemented as the Context class. All we need to do in the **Grid** is to define our services (beans) and create the container instance:

Grid.ts

export class Grid {
    private context: Context;
...

    constructor() {
    ...
        const contextParams = {
            beans: [
                ColumnController, GridApi, EventService,
                ...moduleBeans
            ],
            ...
        };

        this.context = new Context(contextParams, ...);
    }
}

With the introduction of DI container the code has become much more readable, easier to reason about and test.

Before we take a look at how it all it works under the hood, here’s an interesting remark regarding the terminology that I found here. Throughout this article I’ve been using the term Dependency Injection Container, whereas Inversion of Control Container might be a more suitable name. You can read a linked article for more details, but here’s the gist:

By using the word injection [the mechanism] emphasizes the process of constructing the graph of dependencies, ignoring everything else the container does, especially the fact that it manages the whole lifetime of the object, not just its construction.

Make your own mind about terminology. We’re now ready to dive into the implementation details of the DI (IoC) container inside ag-Grid.

High-level implementation details of DI (IoC) Container in ag-grid

All the containers must provide easy support for the following DI lifecycle.

  • Configuring the beans
    The container must know which dependency to instantiate when it encounters a particular type. Basically, it must include some way to register type-mapping.
  • Instantiating the bean
    When using the IoC container, we don’t need to create objects manually. The container does it for us.
  • Wiring the beans together
    The container must include some methods to resolve the specified type; the container creates an object of the specified type, injects the required dependencies if any and returns the object.
  • Managing the bean’s entire life-cycle
    The container must manage the lifetime of the dependent objects. and dispose them when they are no longer needed.

Inside ag-Grid stack the Context class represents the IoC container and is responsible for instantiating, configuring, and assembling the services (beans). The container gets its instructions on what objects to instantiate, configure, and assemble by reading configuration metadata. The configuration metadata is provided through TypeScript annotations and JavaScript configuration object available to the context during initialization.

Creating and configuring the container

The container is created inside the constructor of the main **Grid** service when the datagrid is initialized:

Grid.ts

export class Grid {
    private context: Context;
...

    constructor() {
    ...
        const contextParams = {
            beans: [
                ColumnController, GridApi, EventService,
                ...moduleBeans
            ],
            ...
        };

        this.context = new Context(contextParams, ...);
    }
}

As you can see, the configuration object **contextParams**that describes all beans to managed by the container is provided as a constructor parameter.

Configuring beans

To describe a service managed by the container we implemented two core annotations. The **Bean** annotation is used to specify the name of the service inside the container. The **Autowired**annotationspecifies a dependency on a bean. Here’s an example of how they are used:

RowRenderer.ts

@Bean("rowRenderer")
export class RowRenderer extends BeanStub {
    @Autowired("columnController") private columnController: ColumnController;
}

The [**Bean**](https://github.com/ag-grid/ag-grid/blob/646ec8f0f580eb664d411b43ed907efdab64caf7/packages/ag-grid-community/src/ts/context/context.ts#L328) function simply attaches the metadata to the prototype class of the service through the **__agBeanMetaData** property created inside the function **getOrCreateProps**:

Bean.ts

export function Bean(beanName: string): Function {
    return (classConstructor: any) => {
        const props = getOrCreateProps(classConstructor);
        props.beanName = beanName;
    };
}

The [**Autowired**](https://github.com/ag-grid/ag-grid/blob/646ec8f0f580eb664d411b43ed907efdab64caf7/packages/ag-grid-community/src/ts/context/context.ts#L347) function simply adds the dependency object name to the **agClassAttributes** array on the **__agBeanMetaData** property:

utils.ts

function autowiredFunc(...) {
...
    props.agClassAttributes.push({
        attributeName: methodOrAttributeName,
        beanName: name,
        optional: optional
    });
}

The information specified in annotations is used by the container when it instantiates the services.

Instantiating and wiring the beans

When the container is being created, it automatically creates and wires the beans:

Container.ts

export class Context {
...
    public constructor(params: ContextParams, logger: ILogger) {
    ...
        this.createBeans();
        const beanInstances = this.getBeanInstances();
        this.wireBeans(beanInstances);
    }
}

The **createBeans** function creates instance of the beans that need to be managed by the container. Once the container has all the beans instantiated, it’s ready to wire them together. The function wireBeans wires the beans together and calls some lifecycle methods like **preConstructMethods**and **postConstructMethods**.

The wiring is implemented as the [**autoWireBeans**](https://github.com/ag-grid/ag-grid/blob/03fa336d353ca14786588cc273bf166b38609b56/packages/ag-grid-community/src/ts/context/context.ts#L171) function that is called by the **wireBeans**. The function simply runs over all services, extracts meta information about the dependencies from the **agClassAttributes**, finds the dependency service and assigns it to the respective property defined by metadata. Here’s how it all looks in the code:

Container.ts

private autoWireBeans(beanInstances: any[]): void {
    beanInstances.forEach(beanInstance => {
        this.forEachMetaDataInHierarchy(beanInstance, (metaData: any, beanName: string) => {
            const attributes = metaData.agClassAttributes;
            if (!attributes) {
                return;
            }

            attributes.forEach((attribute: any) => {
                const otherBean = this.lookupBeanInstance(beanName, attribute.beanName, attribute.optional);
                beanInstance[attribute.attributeName] = otherBean;
            });
        });
    });
}

Because the container creates all services eagerly when the container is instantiated, there’s no need to construct a dependency graph that might be required if some beans are created only when they are requested.

Container API

If needed, the container implements small set of API methods used to get an instance of the bean or destroy the container. Here’s an example of requesting the **eventService** from the container:

context.ts

private dispatchGridReadyEvent(gridOptions: GridOptions): void {
    const eventService: EventService = this.context.getBean('eventService');
...
}

When the **destroy** is called on the container instance, it calls **preDestroyMethods** lifecycle method on the beans.

Recommended Reading

How to use Google’s fully typed API SDK in Angular web application

RxJS Subjects in Depth