Power of Eloquence

Dependency Injection - what are they and why should we care (or not) for software architecture design

| Comments

As great software engineers we’re aspired to be every day in our daily work, I’ve always used to think how we can build amazing products for our clients to use so it helps to achieve their major goals tenfold. Especially when you have the scalability forethought in mind.

Built to scale as they say in the world of startups and venture capital funding.

That product can be anything from a simple portfolio website for an artists/singer, a basic space invaders game for kids to play online, to building high-grade commercial e-commerce system for thousands, if not millions of online customers to interact and use worldwide, or perhaps build the next Facebook-scaled size social media platform!

These atypical software products we’re so used to building can vary in size. A product can do one or several simple things. Or a product that makes up so many moving parts that are, rightfully so, considered as components that do very complex jobs on its own. Thus the same product is a behemoth size project so you got think how a lone developer is going to meander through the layers of architecture ensuring that all of these components can work with each other in which they primarily function or not.

Thus it brings to my attention on this very important subject matter - using dependency injection as one of your core software design principles.

Before we get into that terminology, let’s figure what dependency injection really means based on this wiki quote.

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A “dependency” is an object that can be used, for example, a service. Instead of a client specifying which service it will use, something tells the client what service to use. The “injection” refers to the passing of a dependency (a service) into the object (a client) that would use it.

To better explain the above, let’s use the following code setup as a way to demonstrate.

When class A uses some functionality of class B, then it’s said that class A has a dependency of class B.

/* Illustration of Class A depends on Class B*/
// A.js
import B;

class A {
    b = null;
    constructor() {
        this.b = new B()
    }
   // now we have A's methods that depend of B methods/properties
   doSomethingWith = (params = {}) =>{

     if(b.foo) {
       b.doSomething(params)
     }
   }
  // more A's methods
   actionSomethingWith = (params = {}) => {

     if(b.bar) {
      b.actionSomethingWith(params);
    }

   }
}

// B.js
Class B {
   let foo, bar;
    constructor() {
     //some properties to go in here.
     this.foo = 1;
     this.bar = 1
   }
}

const A = new A();
const B = new B();

A.doSomethingWith({a:”foo”, b:”bar”})
A.actionSomethingWith({1:”!”,2:”@“})

Now with the above example here, we have two distinct classes that communicate with one another.

Here, A is dependent on B’s methods, inputs and properties etc to do some important tasks. In the majority of cases, this is fine as it stands if these snippets of code are not poised to be tested or changed frequently in the future.

But more often than not, it’s not always the case so the code needs to evolve. Thus, the problem becomes more apparent when you start to change internal behaviour of class B. If any of the properties of class B is modified such foo or bar are renamed or its methods signature are modified or removed, then the impact of its changes will affect class A for the whole lot as well because everywhere in A has to be changed as we have hardcoded B’s dependency into A.

If you imagine, should we write software like this for every class that depends on each other for performing certain functions or methods such as A is dependent on B, B is dependent on C, C is dependent on D, D is dependent on E etc, etc. This would become an utter nightmare to maintain and change over time thus doing this way leaves you no room for flexibility of swapping or removing dependencies in between, at will.

Thus, what we have gotten ourselves here is an obvious tight coupling of sub-systems with one other. Thus, in usual cases, having tightly coupled systems does not bode for well future incremental changes smoothly over time. By that time, you, as a software engineer, wanted to make several big modifications to the system, you’re more likely going to have to re-architect the entire system inside out just as you first started building it from scratch - day one!

This is not the ideal situation to be in.

Hence, for a long time, in the tech industry, we have a number of battled-tested industry-standard software patterns that deals with this to make our software grown to scale and change incrementally and progressively. And one of them is Dependency injection (DI).

So the question beckons - why does DI matter here?

The long-winded but simple answer to this question is when you have lots of classes that need to talk to each other (such as above), want to keep your code organised, and make it easy to swap different types of objects in/out of your application that gets to communicate with one another at run time.

DI is the main person that does all the work for you.

The key benefit of using this pattern is to encourage loose coupling. Objects can be added(or removed) and tested independently of other objects because they don’t depend on anything other than what you pass onto them. When using traditional dependencies, to test an object you have to create an environment where all of its dependencies exist and are reachable before you can test it. In DI, you can test an object in isolation by passing mock objects for the ones you don’t need or want to create.

At present, our DI pattern strategies come in three types to choose from.

To illustrate these types, let us use Car analogy for this.

Constructor Injection

First one is that dependencies are provided through a class constructor

/* Constructor Injection DI example*/

function Piston (energy, size, noOfPistons) {
    this.energy = energy;
    this.size = size;
    this.noOfPistons = noOfPistons;
}

function Car (wheels, engine) {
  this.wheels = wheels;
  this.engine = engine;
}

Car.prototype.start = function() {
   if(this.engine) {
      this.engine.start();
   }
}

Car.prototype.move = function(direction) {
   If(this.wheels) {
       this.wheels.move(direction);
   }
}

function Engine (pistons) {
    this.pistons = pistons
}

Engine.prototype.start() = function() {
  if(this.pistons) {
    console.log('Engine is starting...')
  }
}

function Wheels(size, shape, noOfWheels) {
   this.size = size;
   this.shape = shape;
   this.noOfWheels = noOfWheels;
}

Wheels.prototype.move = function(direction) {
    console.log('Wheels are moving +  ' + direction);
}


let wheels = new Wheels(8, 'round', 3)
let pistons = new Pistons(400, 10, 10)
let engine = new Engine(pistons)
let car = new Car(wheels, engine);

car.start(); // it will say Engine is starting as long as the car got a working engine that's got some working pistons as well.
car.move('forward');  // it will say Wheels are moving forward as long as the car has wheels

The example above illustrates that our dependencies injected through class constructor. As far as our above code section is written in Javascript, we got Car constructor that has been injected with Wheels and Engine dependency, along with Engine constructor injected with Pistons dependency. The dependency creation is no longer hardcoded on the constructor level. The constructor classes are no longer responsible for instantiating dependencies’ attributes upon demand. Thus it does not care how they’re built in the first place. They are just required as they see fit.

Setter Injection

The second type, as the name suggests, the client exposes a setter method that injector uses to inject the dependency. By having injector we mean the DI guy, as mentioned earlier part of this post. To accomplish this, you need some of a decent injector library that does the work underneath whose main task is to register all the dependencies to one or several external client code and supply them on demand during run time.

To illustrate this, here’s the following example written in Javascript.

/* Setter Injection DI example */

Car.prototype.setEngine = function(engine) {
   this.engine = engine;
   return this;
}

Car.prototype.setWheels = function(wheels) {
  this.wheels = wheels;
  return this;
}

Engine.prototype.setPistons() = function(Pistons) {
  this.pistons = pistons;
  return this;
}

//Introducing our injector personnel..
const the_di_guy = require('some-di-injector-library');

the_di_guy
   .register('piston')
       .as(Piston)
       .withConstructor()
       .params().val(400,10,10)
   .register('engine')
        .as(Engine)
        .withConstructor()
        .withProperties()
        .func('setPistons')
       .param().ref('pistons')
   .register('wheels')
      .as(Wheels)
     .withConstructor()
     .params().val(8, 'round', 3)
  .register('car')
    .as(Car)
    .withConstructor()
    .withProperties()
    .func('setEngine')
    .param().ref('engine')
    .func('setWheels')
    .param().ref('wheels')

Here we have Car and Engine objects use setter methods in injecting their required dependencies and returns their prototype function methods respectively. Why are we doing this? We’re doing this because our imaginary DI library here looks at all available prototype methods provided by various modules or sub-systems it’s fully aware of after registering them within the environment thus inject dependencies based on the setter injection rules we set up in beginning.

Interface Injection

Lastly, this is where the dependency provides an injector method that will inject the dependency into any client passed to it. Client must implement an interface that exposes a setter method that accepts the dependency.

Unfortunately, at the time of writing, there’s no such pattern exists in the JS world as JS itself does not do interfaces compared to other traditional object-oriented programming languages such as Java and C#. JS and any other dynamically-type languages do not require interfaces because they’re ‘replaced’ with late binding/duck typing. For actual examples of interface injection done in Java, it would look something like this.

// for the car
package some.package;
public class Car implements EngineMountable {
    private Engine engine;
    private Wheels wheels;

    @Override //dependency injection
    public void setEngine(Engine engine){
        this.engine = engine;
    }

    @Override //dependency injection
    public void setWheels(Wheels wheels){
        this.wheels = wheels;
    }
}

public interface EngineMountable {
    void setEngine(Engine engine);
}

public interface WheelsMountable {
   void setWheels(Wheels wheels);
}
//for the engine
public class Engine implements PistonsMountable {
  private Pistons;
  
      @Override //dependency injection
    public void setPistons(Pistons pistons){
        this.pistons = pistons;
    }
}

public interface PistonsMountable {
   void setPiston(Pistons pistons);
}

```  

Now, at this point, you may be thinking.  

If you come from the world of dynamic languages like Javascript for a considerable time, and the fact is many of the DI information presented are, in actual fact, the prime use cases used in the traditional object-oriented languages like Java and C# really, you might start to beg this question....

Does DI truly still pose relevance to this modern age of software architecture design moving forth, considering how many JS and NodeJS frameworks are made every day of the week?

The answer is yes and no.

It depends on who and what you've heard from the software developer veteran community lately and which programming "tribe" you belong to.

For eg, let's take on two popular JS frameworks; React and Angular.

Let's start with Angular cause it's the easier and most likely candidate that benefits from DI.

By in large, Angular is itself, a DI framework if not just the framework to build client-side apps all the time.  Because Di is built-in, Angular has the robust structure of how injectors should allow dependencies to be loaded (or removed) for certain components that require such dependencies' services.  Angular provides a mechanism for components to delegate certain tasks for certain services that they should not have any concerns about other than just actioning optimal user experiences.  These delegated services are phrased as injectable service classes.

For some examples, here are the common uses written in Typescript.

``` javascript
// Logger class service in Typescript

export class Logger {
  log(msg: String) { console.log(msg);}
  error(msg: String) { console.error(msg);}
  warn(msg: String) { console.warn(msg);}
}

Somewhere in the app, Logger class is used.

//A service class that does funky work

export class SomeFunkyService {

  // services instances are injected here via constructor
  constructor(private logger: Logger, private backendService: BackendService){}

  // we can pretend it makes good funky ice cream k yeah? =)
  makeMeAFunkyIceCream(flavours) {
    this.backendService.makeFunkyIceCream(flavours).then( (icecream: IceCream) => {
       this.logger.log('Ice cream with funky' + flavours + 'is made. Wahoo!!')
       return icecream;
    }
  }
}

Here, you notice it the way Logger service is injected uses the constructor injection strategy as per our examples earlier.

Not much different concept. The difference is that Angular has its built-in injector that looks at all available instances of dependencies during its boot-up time. These dependencies are created through registration which is called a provider. The way providers get registered can be done in several ways due to the hierarchical setup of Angular injector system, ie it can be provided at root, the module level or the component level etc. Which is the core reason why Angular is extremely attractive to back-end developers written in static languages ie Java/C#/etc for a long while now.

For the simplicity and scope of the post, we use the root level approach for this injection, which is simply.

@Injectable({
   providedIn: 'root'
})

export class Logger{}

That’s DI in the Angular in a nutshell!

Now how about React? Does React have common DI concept to embrace as well?

Apparently… it does not! Why? Because it doesn’t need to.

It just handles dependencies on-demand creation differently.

Thank a look at this small snippet React code:


const ProductReviewList = props => (
     <List resource="reviews" perPage={50} {...props}>
        <Datagrid rowClick="edit">
            <DateField source="date" />
            <CustomerField source="customer_id" />
            <ProductField source="product_id" />
            <RatingField source="rating" />
            <TextField source="body" label="Comment"/>
            <StatusField source="status" />
        </Datagrid>
    </List>
)

If you’ve worked with React for some time, we always talk about building components and each component that can be made up of other components through parent-child-sibling hierarchy relationship such as List, DataGrid and DateField components etc.

In the object-oriented world, we treat components as objects thus in order for components that depend on one another through the use of constructor and injector setup. But in the world of React, this is not necessarily the case, because you’ll often find that the child component does not always depend on the structure of the parent components such as Datagrid and List components respectively to perform its necessary duties. Here, we’re saying we have List component has some data that comes through props. But it does not display that same data as a list of reviews. Rather, it delegates the rendering work to the Datagrid component. We’ve injected our dependencies through the powers of composition without needing for injectors or constructor/setter injectors like other object-oriented languages do.

The beauty with this setup is that you can easily swap dependency around at different places without worrying too much the consequence of changing the hierarchy order of the dependencies between components.

Thus with the List above, I can replace the Datagrid component to something like CardView component;

const ProductReviewCard = props => (
    <List resource='products' perPage={10}>
      <CardView
          mainHeading={review => <Card record={review}/>}
          mainBody={review => review.body}
      >
    </List>
)

All of these are possible thanks to the powerful concepts such as JSX and props patterns, and many other core React patterns that allows you to encapsulate better dependency management in the React world vs more traditionally object-oriented based system design like Angular/C#/Java etc, etc.

Parting thoughts

So there you have it, folks!

That’s what Dependency Injection is all about.

In summary, the key long-term benefits of using DI are stressed as follows:

  1. Reduced dependency creation
  2. Reduced dependency carrying betwen modules
  3. Unit testing are easier to accomplish with these indepedent modules
  4. Encourage code reuse
  5. Encourage loose coupling system

Especially the last point is very important as the software design always keep changing to scale and evolve nowadays. They rarely ever stay in a fixed hence tight-coupling systems has no place for them.

However, one thing I failed to explain earlier in this post is to find out when do we really need not care about using DI when designing our software architecture.

Well.

The simple answer lies in the assumption that how many subsystems or modules we know are going to be frequently changed/swapped/configured upfront during its implementation. If they’re not highly configurable, reusable and unit testing has no place for concern or there’s little need to worry about coupling, then we don’t need to hire DI middle guy to do the work at all. It can just work fine on its own.

And it depends on the type of programming languages you work with intimately as you’re probably aware that DI’s inseparably hugely popular for classic object-oriented statically typed programming languages such as Java and C#. But not very much so for dynamically typed languages such as JS and Python. You can read more about it here on this Stackoverflow link.

Disclaimer:

On a final note, I’m do not claim myself as a DI/IOC designer nor am I subject matter expert on it. I’m just an ordinary software guy who’s got the knack and avid curiosity about the software world and how everything works from reading forums to build things in my own spare time which allows me to share my knowledge and learnings from my past React/NodeJS/Angular project work that has DI built inside with everyone keen to understand how difficult concepts such as these can fully be grasped with simple use case examples people online can relate with. Software development/engineering with the forethoughts of agile principles is always part of the software craftsman journey that truly never ends. That’s the fun part of it. :)

Till then, Happy Coding!

PS: Useful References that inspired me to write up this post to document my learning process.

Comments