Most flexible systems are those in which dependencies refer only to abstractions, not to concretions.
In statically typed languages, this means that import/include statements should refer only to source modules containing interfaces/abstract classes.
DIP leverages Stable Abstractions and Concrete Components concepts.
In dynamically typed languages, source code dependencies should not refer to concrete modules (functions already implemented).
It is the volatile concrete elements of our system that we want to avoid depending on. Those are the modules that we are actively developing, and that are undergoing frequent change.
In conventional application architecture, lower-level components (e.g. Utility Layer) are designed to be consumed by higher-level components (e.g. Policy Layer). In this composition, higher-level components depend directly upon lower-level components to achieve some task. This dependency upon lower-level components limits the reuse opportunities of the higher-level components.
The hoal of DIP is to avoid this highly coupled distribution with the mediation of an abstract layer, and to increase the re-usability of higher/policy layers.
We tend to ignore the stable background of operating system and platform facilities when it comes to DIP, because we know we can rely on them not to change.
A direct implementation packages policy classes with service abstracts classes in one library. In this implementation high-level components and low-level components are distributed into separate packages/libraries, where the interfaces defining the behaviors/services required by the high-level component are owned by, and exist within the high-level component's library.
The implementation of the high-level component's interface by the low-level component, requires that the low-level component package depend upon the high-level component for compilation, thus inverting the conventional dependency relationship.
In this version of DIP, re-utilization of lower layer components is difficult.
//== Layer 1 - PROGRAMMER: HIGH-LEVEL COMPONENT //==
class Programmer {
fullName: string;
language: string;
computer: Computer;
constructor(fullName: string, language: string, computer: Computer) {
this.fullName = fullName;
this.language = language;
this.computer = computer;
}
startCoding() {
this.computer.powerOn();
console.log(`${this.fullName} is coding in ${this.language}`);
}
}
//== Layer 1 - PROGRAMER SERVICES ==//
interface IComputer {
powerOn(): void;
}
//== LOW LEVEL COMPONENT ==//
class Computer {
private this._count = 0;
constructor() {
this.id = ++this._count;
}
powerOn() {
console.log(`Computer #${this.id} powering...`);
}
}
//== Orchestator ==//
const computer = new Computer();
const programmer = new Programmer('Nacho', 'JS', computer);
programmer.startCoding();
A more flexible solution extracts the abstract components into an independent set of packages/libraries.
The separation of each layer into its own package encourages re-utilization of any layer, providing robustness and mobility.
//== Layer 1 - PROGRAMMER: HIGH-LEVEL COMPONENT //==
class Programmer {
fullName: string;
language: IProgrammingLanguage;
computer: IComputer;
constructor(fullName: string, language: IProgrammingLanguage, computer: IComputer) {
this.fullName = fullName;
this.language = language;
this.computer = computer;
}
startCoding() {
this.computer.powerOn();
console.log(`${this.fullName} is coding in ${this.language.name}`);
this.language.run();
}
}
//== Layer 1 - PROGRAMER SERVICES ==//
interface IProgrammer {
fullName: string;
language: IProgrammingLanguage;
computer: IComputer;
}
interface IComputer {
powerOn(): void;
}
interface IProgrammingLanguage {
name: string;
run(): void;
}
//== Layer 2 - Computer: LOW LEVEL COMPONENT ==//
class Computer {
private this._count = 0;
constructor() {
this.id = ++this._count;
}
powerOn() {
console.log(`Computer #${this.id} powering...`);
}
}
//== Layer 2 - ProgrammingLanguage: LOW LEVEL COMPONENT ==//
class ProgrammingLanguage {
constructor(name: string): IProgrammingLanguage {
this.name = name;
}
run() {
console.log(`Running -${this.name}- code`);
}
}
//== Orchestator ==//
const computer = new Computer();
const javascript = new ProgrammingLanguage('Javascript');
const programmer = new Programmer('Nacho', javascript, computer);
programmer.startCoding();
The creation of volatile concrete objects requires special handling (_ Abstract Factories_).
The curved line is an architectural boundary, it divides the system into two components, abstract and concrete.
Abstract components contain all the high-level business rules of the application.
Concrete components contain all the implementation details that those business rules manipulate.
//== Layer 1 - PROGRAMMER: HIGH-LEVEL COMPONENT //==
class Programmer {
fullName: string;
computer: IComputer;;
monitor: IMonitor;
constructor(fullName: string, computer: IComputer, monitor: IMonitor): IProgrammer {
this.fullName = fullName;
this.computer = computer;
this.monitor = monitor;
}
startCoding() {
this.computer.powerOn();
this.monitor.powerOn();
console.log(`${this.fullName} is coding.`);
}
}
//== Layer 1 - PROGRAMER SERVICES ==//
interface IProgrammer {
fullName: string;
computer: IComputer;
monitor: IMonitor;
}
interface IComputer {
brand: string;
powerOn(): void;
}
interface IMonitor {
brand: string;
powerOn(): void;
}
interface IToolsFactory {
buildComputer(): IComputer;
buildMonitor(): IMonitor;
}
//== Layer 2 - Computer: LOW LEVEL COMPONENT ==//
class Computer {
brand: string;
private this._count = 0;
constructor(brand: string): IComputer {
this.id = ++this._count;
this.brand = brand;
}
powerOn() {
console.log(`Computer #${this.id}-${this.brand} powering...`);
}
}
//== Layer 2 - Monitor: LOW LEVEL COMPONENT ==//
class Monitor {
brand: string;
private this._count = 0;
constructor(brand: string): IMonitor {
this.id = ++this._count;
this.brand = brand;
}
powerOn() {
console.log(`Monitor #${this.id}-${this.brand} powering...`);
}
}
//== Layer 2 - ToolFactory: LOW LEVEL COMPONENT ==//
class AppleToolFactory {
buildComputer(): IComputer {
console.log('Building Macbook');
return new Computer('Apple');
}
buildMonitor(): IMonitor {
console.log('Building AppleTV');
return new Monitor('Apple');
}
}
//== Orchestator ==//
const appleFactory = new AppleToolFactory();
const programmer = new Programmer(
'Nacho',
appleFactory.buildComputer(),
appleFactory.buildMonitor(),
);
programmer.startCoding();
The use of interfaces to accomplish DIP has design implications in object-oriented software:
- All member variables in a class must be interfaces or abstracts.
- All concrete class packages must connect only through interface or abstract class packages.
- No class should derive from a concrete class.
- No method should override an implemented method.
- All variable instantiation requires the implementation of a Creational Pattern such as the Factory Method or the Factory pattern, or the use of a Dependency-Injection framework.