Design Patterns
- Dependency Injection
"Dependency injection is a software design pattern that allows the removal of hard-coded dependencies and makes it possible to change them, whether at run-time or compile-time."
-wiki.
It allows us to standardize and centralize the way objects are constructed in our application by supplying (injecting) an external dependency into a software component rather than creating an dependency within.
Here is a brief description from https://docs.angularjs.org/guide/di. Here is some samples of using it in AngularJS, AngularJS - Dependency Injection.
There are three ways a component (object or function) can get a hold of its dependencies:
- The component can create the dependency, typically using the new operator.
- The component can look up the dependency, by referring to a global variable.
- The component can have the dependency passed to it where it is needed.
The first two options of creating or looking up dependencies are not optimal because they hard code the dependency to the component. This makes it difficult, if not impossible, to modify the dependencies. This is especially problematic in tests, where it is often desirable to provide mock dependencies for test isolation.
The third option is the most viable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component.
Making a class's dependencies explicit and requiring that they be injected into it is a good way of making a class more reusable, testable and decoupled from others. There are several ways that the dependencies can be injected:
- Setter Injection
Adding a setter method that accepts the dependency is one way of injection point into a class.
Pros:- Setter injection works well with optional dependencies. If we do not need the dependency, then just do not call the setter.
- We can call the setter multiple times. This is particularly useful if the method adds the dependency to a collection. We can then have a variable number of dependencies.
- The setter can be called more than just at the time of construction so we cannot be sure the dependency is not replaced during the lifetime of the object (except by explicitly writing the setter method to check if has already been called).
- We cannot be sure the setter will be called and so we need to add checks that any required dependencies are injected.
- Constructor Injection (the most popular way of injection)
The most common way to inject dependencies is via a class's constructor. To do this, we need to add an argument to the constructor signature to accept the dependency.
There are several advantages to using constructor injection:
- If the dependency is a requirement and the class cannot work without it then injecting it via the constructor ensures it is present when the class is used as the class cannot be constructed without it.
- The constructor is only ever called once when the object is created, so we can be sure that the dependency will not change during the object's lifetime.
- Property Injection
Just setting public fields of the class directly:
The disadvantages of using property injection are similar to setter injection but with the following additional important problems:- We cannot control when the dependency is set at all, it can be changed at any point in the object's lifetime.
- We cannot use type hinting so we cannot be sure what dependency is injected except by writing into the class code to explicitly test the class instance before using it.
Here is a video from Youtube:
In the code below, the application (Drawing class) is drawing a specified shape.
#include <iostream> #include <string> using namespace std; class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() { cout << "circle\n"; } }; class Triangle : public Shape { public: void draw() { cout << "triangle\n"; } }; class Drawing { public: void drawShape(string s) { if(s == "triangle") pShape = new Triangle; else if( s == "circle") pShape = new Circle; pShape->draw(); } private: Shape *pShape; };
If the drawShape() method is given "triangle" as its parameter, it will draw triangle, if "circle" is given, then it will draw circle. However, in software design perspective, the fact that the instantiation is done within the class itself, the Drawing class is tightly coupled with Triangle and Circle classes.
What problems does dependency create?
- Code is tightly coupled.
- Difficult to isolate the code to test.
- Maintenance cost is high.
If I change code A, how do I know what else did I break?
Now, we want to modify the previous sample code so that we can break the coupling between Drawing and Triangle/Circle classes.
How?
#include <iostream> #include <string> using namespace std; class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() { cout << "circle\n"; } }; class Triangle : public Shape { public: void draw() { cout << "triangle\n"; } }; /* 1. This class does not have hard-coded shapes such as Triangle and Circle 2. So, it's decoupled and has no dependency 3. The specific information is injected by other class 4. This code can be remained untouched when we switch the shape to draw */ class Drawing { public: void drawShape(Shape *pShape) { pShape->draw(); } private: Shape *pShape; }; /* 1. This class pulled the hard-coded shape info out of the Drawing class (compare with the previous example) 2. This class is an interface that can be modified depending on what to draw 3. This class is doing the dependency injection */ class IDrawing { public: IDrawing() { d = new Drawing; } void draw(string s) { if(s == "triangle") d->drawShape(new Triangle); else if( s == "circle") d->drawShape(new Circle); else cout << " Need shape"; } private: Drawing *d; };
As we can see from the code, the Drawing class does not have hard-coded shapes anymore. Now, it's decoupled and has no dependency. The specific shape information is injected by other interface class (IDrawing) and it does not instantiate anything within the class. Therefore, this code can be remained untouched even though we switch the shape to draw.
The interface class IDrawing pulled the hard-coded shape info out of the Drawing class. This class as an interface can be modified depending on what to draw, and it is doing the dependency injection.
- Pros
- Loosely coupled code
- Increase testibility
- Can use IoC container
- Cons
- Increase code complexity
- Hard to understand code, at least initially
A dependency injection is a software design pattern popularized by a software engineer named Martin Fowler. The main principle behind dependency injection is the inversion of control in a software development architecture. To understand this better, let's have a look at the following notifier example:
var Notifier = function() { this.userService = new UserService(); }; Notifier.prototype.notify = function() { var user = this.userService.getUser(); if (user.role === 'admin') { alert('You are an admin!'); } else { alert('Hello user!'); } };
Our Notifier class creates an instance of a userService, and when the notify() method is called, it alerts a different message based on the user role. Now this can work pretty well, but what happens when we want to test our Notifier class?
We will create a Notifier instance in our test, but won't be able to pass a mock userService object to test the different results of the notify method. Dependency injection solves this by moving the responsibility of creating the userService object to the creator of the Notifier instance, whether it is another object or a test. This creator is often referred to as the injector. A revised version of this example will be as follows:
var Notifier = function(userService) { this.userService = userService; }; Notifier.prototype.notify = function() { var user = this.userService.getUser(); if (user.role === 'admin') { alert('You are an admin!'); } else { alert('Hello user!'); } };
Now, whenever we create an instance of the Notifier class, the injector will be responsible for injecting a userService object into the constructor, making it possible to control the behavior of the Notifier instance outside of its constructor, a design often described as inversion of control.
This section is from - MEAN Web Development by Amos Q. Haviv
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization