This is the 1st part of the series of understanding SOLID Principles where we explore what is Dependency Inversion and why it helps deliver software that is loosely coupled and testable.
When reading books or talking to other developers, you might come across or heard the term SOLID.
In that kind of discussion, some people mention the importance of it and how the ideal system will need to have all of the characteristics of those principles.
When working in everyday scenarios you might not have the time to reason with the architecture or how to incorporate good design decisions without hitting the time limit or the voice of your boss.
However, these principles are not just for skipping them. Software Engineers should apply them to their development efforts. The real question every time you type code is how do you correctly apply the principles, so that will make your code more elegant.
In SOLID there are five basic principles which help to create good (or solid) software architecture. SOLID is an acronym where:-
S stands for SRP (Single responsibility principle)
O stands for OCP (Open closed principle)
L stands for LSP (Liskov substitution principle)
I stand for ISP ( Interface segregation principle)
D stands for DIP ( Dependency inversion principle)
Originally compiled by Robert C. Martin back in the 1990s, these principles provide a way to construct software components that are tightly coupled code with poor cohesion and little encapsulation to the complete opposite; loosely coupled code, cohesive and truly encapsulating the real needs of the business appropriately.
Good practice is anything that reduces coupling, which improves testability, maintainability, and replace-ability.
This is an important point. Principles will not turn a bad programmer into a good programmer. Principles have to be applied with judgment. If they are applied by arbitrarily it is just as bad as if they are not applied at all.
This is not only about design patterns any more. It’s about thorough evaluation of each domain problem and exercising pragmatic solutions to avoid critical code smells.
Knowledge of the principles and patterns gives you the justification to decide when and where to apply them. Although those principles are mainly heuristics, they are common-sense solutions to common problems. In practice, they have been proven over and over and over again. Thus they should be approached with common sense.
For the remaining of this article, I will begin exploring the Dependency Inversion Principle.
Dependency Inversion.
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
What does that tell you? On the one hand, you have Abstractions. In software engineering and computer science, abstraction is a technique for arranging complexity of computer systems. It works by establishing a level of complexity on which a person interacts with the system, suppressing the more complex details below the current level. Its scope is broad and covers more than one subsystem.
Thus when working with abstractions you work on a high-level view of your system. You only care about the interactions you can do and not how to do them.
On the other hand, you have low-level modules or Details. Those are the opposite. They are programs written to solve a particular problem. Their scope is limited and often cover a unit or a subsystem. For example opening a MySQL database connection is considered low level as its bound to a specific scope.
Now by reading out those 2 rules what can you imply?
The real intent behind dependency Inversion is to decouple objects to the extent that no client code has to be changed simply because an object it depends on needs to be changed to a different one. That achieves loosely coupling as each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. It achieves testability and replaceability because components in a loosely coupled system can be replaced with alternative implementations that provide the same services.
The drawback of dependency Inversion is that you need a dependency injection container, and you need to configure it. This container will have the required capabilities to inject your services in the required places with the right scope and the right parameters.
Great! So how do I start?
We can learn more about dependency Inversion, in practice using an awesome library in Javascript called Inversify.js.
Inversify.js. A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.
In this example, I will show you how to provide services for a WebSocket connection.
Let's say for example that you have a web server that exposes WebSockets and clients that want to connect to the server and receive updates. Currently, there are several solutions for providing a plain WebSocket. There is Socket.io, Socks, a plain WebSocket, etc … and each one of them has a different API or different methods you can call. Would it be nice if we had somehow abstracted the whole idea of a WebSocket provider in a common interface? That way we can provide a different WebSocket creator or SocketFactory that accepts some kind of Configuration and creates a WebSocket connection depending on our needs.
First, let's define our interfaces:
export interface WebSocketConfiguration {
uri: string;
options?: Object;
}
export interface SocketFactory {
createSocket(configuration: WebSocketConfiguration): any;
}
Notice that there is nothing concrete here just Interfaces. We say that those are our Abstractions.
Now let's say we want a Socket-io Factory:
import {Manager} from 'socket.io-client';
class SocketIOFactory implements SocketFactory {
createSocket(configuration: WebSocketConfiguration): any {
return new Manager(configuration.uri, configuration.opts);
}
}
This is something concrete and not abstract anymore because it specifies a Manager from Socket-io library. That's our Details.
We could add many more Factory Classes as long as they implement the SocketFactory interface.
Now that we have our Factory we need a way to provide an Abstraction over this WebSocket implementation what will not depend on that particular Instance.
Let's start again with a new Abstraction:
export interface SocketClient {
connect(configuration: WebSocketConfiguration): Promise<any>;
close(): Promise<any>;
emit(event: string, ...args: any[]): Promise<any>;
on(event: string, fn: Function): Promise<any>;
}
Let's provide a Detailed view of our Abstraction:
class WebSocketClient implements SocketClient {
private socketFactory: SocketFactory;
private socket: any;
public constructor(webSocketFactory: SocketFactory) {
this.socketFactory = webSocketFactory;
}
public connect(config: WebSocketConfiguration): Promise<any> {
if (!this.socket) {
this.socket = this.socketFactory.createSocket(config);
}
return new Promise<any>((resolve, reject) => {
this.socket.on('connect', () => resolve());
this.socket.on('connect_error', (error: Error) => reject(error));
});
}
public emit(event: string, ...args: any[]): Promise<any> {
return new Promise<string | Object>((resolve, reject) => {
if (!this.socket) {
return reject('No socket connection.');
}
return this.socket.emit(event, args, (response: any) => {
if (response.error) {
return reject(response.error);
}
return resolve();
});
});
}
public on(event: string, fn: Function): Promise<any> {
return new Promise<any>((resolve, reject) => {
if (!this.socket) {
return reject('No socket connection.');
}
this.socket.on(event, fn);
resolve();
});
}
public close(): Promise<any> {
return new Promise<any>((resolve) => {
this.socket.close(() => {
this.socket = null;
resolve();
});
});
}
}
Notice how we pass a parameter of type SocketFactory to the constructor. This is the first Rule of dependency Inversion. For the second rule, we need a way to provide this value that is both easy to replace or configure without knowing details about it.
That's where Inversify comes in and manages this kind of control. Let's add some annotations to the mix:
import {injectable} from 'inversify';
const webSocketFactoryType: symbol = Symbol('WebSocketFactory');
const webSocketClientType: symbol = Symbol('WebSocketClient');
let TYPES: any = {
WebSocketFactory: webSocketFactoryType,
WebSocketClient: webSocketClientType
};
@injectable()
class SocketIOFactory implements SocketFactory {...}
...
@injectable()
class WebSocketClient implements SocketClient {
public constructor(@inject(TYPES.WebSocketFactory) webSocketFactory: SocketFactory) {
this.socketFactory = webSocketFactory;
}
Those annotations just add additional metadata on how to provide all of those components at runtime. What it needs to be done now is just create our Dependency Inversion container and bind everything in the right type.
import {Container} from 'inversify';
import 'reflect-metadata';
import {TYPES, SocketClient, SocketFactory, SocketIOFactory, WebSocketClient} from '@web/app';
const provider = new Container({defaultScope: 'Singleton'});
// Bindings
provider.bind<SocketClient>(TYPES.WebSocketClient).to(WebSocketClient);
provider.bind<SocketFactory>(TYPES.WebSocketFactory).to(SocketIOFactory);
export default provider;
That's it! Now every time you would like an instance of a SocketClient you just invoke the container with the right binding.
var socketClient = provider.get<SocketClient>(TYPES.WebSocketClient);
Of course, Inversify offers more than simple bindings. I suggest you head over the website and check it out.
Exercises
1 Find what other libraries exist that provide a Dependency Inversion Container.
In the examples, I did instantiate the container using 'Singleton' Scope. What would happen If I didn't specify that? What another way can I do that using Inversify?
Look over your own projects or websites and think of ways you can Utilize Dependency Inversion on your services. Example API calls, promises, etc.
Extra Credits: Implement your own DI container library. Have a class or a data-structure that accepts keys and values. For keys, you specify a name and for values you specify the instance resolved. Try to add methods for specifying the scope like as a singleton or as a factory.
Recap
I hope you understood what DI is and made you aware of its traits. Stay put for the next article.
References
SOLID Principles
Wiki
Getting a SOLID Start
Another Wiki
From STUPID to SOLID
Coming up next is Understanding SOLID Principles: Single Responsibility
If this post was helpful please share it and stay tuned on my other articles. You can follow me on GitHub and LinkedIn. If you have any ideas and improvements feel free to share them with me.
Happy coding.
Cover Photo by Agustin Lautaro on Unsplash
Top comments (6)
nice write up. not directly related to the topic but I feel that not rejecting promises with anything other than actual Error instance is yucky. ... It would be nice if the whole JS world could agree on that API/Interface ... It would make error handling code alot less error prone (it difficult to know with certainty whether any given
.catch()
handler wont be confronted with a non Error from some deeply nested dependency)inversion != injection
Thank you. Yes they are not the same!
Great tutorial. Thank, Theofanis!
I already like the series. Looking forward to the next one.
Thanks for writing about inversify!