![](/style/images/good.png)
![](/style/images/bad.png)
SOLID: The Dependency Inversion Principle in Angular
source link: https://www.tuicool.com/articles/BJraM3j
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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.
This principle states that classes and modules should depend on abstractions not on concretions.
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
Tip: Use Bit to get the most out of your SOLID Angular project
SOLID code is modular and reusable. With Bit , you can easily share and organize your reusable components. Let your team see what you’ve been working on, install and reuse your components across projects, and even collaborate together on individual components. Give it a try .
What are Abstractions?
Abstractions are Interfaces. Interfaces define what implementing Classes must-have. If we have an Interface Meal:
interface Meal { type: string }
This holds information on what type of meal is being served; Breakfast, Lunch or Dinner. Implementing classes like BreakFastMeal
, LunchMeal
and DinnerMeal
must have the type
property:
class BreakFastMeal implements Meal { type: string = "Breakfast" }class LunchMeal implements Meal { type: string = "Lunch" }class DinnerMeal implements Meal { type: string = "Dinner" }
So, you see Interface gives the information on what properties and methods the class that implements it must have. An Interface is called an Abstraction because it is focused on the characteristic of a Class rather than the Class as a whole group of characteristics.
What are Concretions?
Concretions are Classes. They are the opposite of Abstractions, they contain the full implementation of their characteristics. Above we stated that the interface Meal
is an abstraction, then the classes that implemented it, DinnerMeal
, BreakfastMeal
and LunchMeal
are the concretions, because they contain the full implementation of the Meal
interface. Meal
has a characteristic type
and said it should be a string type, then the BreakfastMeal
came and said the type
is "Breakfast"
, LunchMeal
said the type
is "Lunch"
.
The DIP says that if we depend on Concretions, it will make our class or module tightly coupled to the detail. The coupling between components results in a rigid system that is hard to change, and one that fails when changes are introduced.
Example: Copier
Let’s use an example to demonstrate the effects of using the DIP. Let’s say we have a program that gets input from a disk and copies the content to a flash drive.
The program would read a character from the disk and pass it to the module that will write it to the flash drive.
The source will look like this:
function Copy() { let bytes = [] while(ReadFromDisk(bytes)) WriteToFlashDrv(bytes) }
Yes, that’s a work well done, but this system is rigid, not flexible. The system is restricted to only reading from a disk and writing to a flash drive. What happens when the client wants to read from a disk and write to a network? We will see ourself adding an if statement to support the new addition
function Copy(to) { let bytes = [] while(ReadFromDisk(bytes)) if(to == To.Net) WriteToNet(bytes) else WriteToFlashDrv(bytes) }
See we touched
the code, which shouldn’t be so. As time goes on, and more and more devices must participate in the copy program, the Copy
function will be littered with if/else
statements and will be dependent upon many lower-level modules. It will eventually become rigid and fragile.
To make the Copy function reusable and less-fragile, we will implement interfaces Writer and Reader so that any place we want to read from will implement the Reader interface and any place we want to write to will implement the Write interface:
interface Writer { write(bytes) }interface Reader { read(bytes) }
Now, our disk reader would implement the Reader interface:
class DiskReader implements Reader { read(bytes) { //.. implementation here } }
then, network writer and flash drive writer would both implement the Writer interface:
class Network implements Writer { write(bytes) { // network implementation here } }class FlashDrv implements Writer { write(bytes) { // flash drive implementation } }
The Copy function would be like this:
function Copy(to) { let bytes = [] while(ReadFromDisk(bytes)) if(to == To.Net) WriteToNet(bytes) else WriteToFlashDrv(bytes) } | | vfunction Copy(writer: Writer, reader: Reader) { let bytes = [] while(reader.read(bytes)) writer.write(bytes) }
See, our Copy has been shortened to a few codes. The Copy function now depends on interfaces, all it knows is that the Reader will have a read method it would call to write bytes and a Reader with a read method where it would get bytes to write, it doesn’t concern how to get the data, it is the responsibility of the class implementing the Writer.
This makes the Copy function highly re-usable and less-fragile. We can pass any Reader or Writer to the Copy function, all it cares:
// read from disk and write to flash drive Copy(FlasDrvWriter, DiskReader)// read from flash and write to disk Copy(DiskWriter, FlasDrvReader)// read from disk and write to network ie. uploading Copy(NetworkWriter, DiskReader)// read from network and write to disk ie. downloading Copy(DiskWriter, NetworkReader)
Example: Nodejs Console class
The Nodejs Console
class is an example of a real-world app that obeys the DIP. The Console
class produces output, yeah majorly used to output to a terminal, but it can be used to output to other media like:
- file
- network
When we do console.log(“Nnamdi”)
Nnamdi
is printed to the screen, we can channel the output to another place like we outlined above.
Looking at the Console class
function Console(stdout, stderr) { this.stdout = stdout this.stderr = stderr ? stderr : stdout }Console.prototype.log = function (whatToWrite) { this.write(whatToWrite, this.stdout) }Console.prototype.error = function (whatToWrite) { this.write(whatToWrite, this.stderr) }Console.prototype.write = function (whatToWrite, stream) { stream.write(whatToWrite) }
It accepts a stdout
and stderr
which are streams, they are generic, the stream can be a terminal or file or anywhere like network stream. stdout
is where to write out, the stderr
is where it write any error. The console object we have globally has already been initialized with stream set to be written to terminal:
global.console = new Console(process.stdout, process.stderr)
The stdout and stderr are interfaces that have the write method, all that Console knows is to call the write method of the stdout and stderr.
The Console depends on abstracts stdout and stderr, it is left for the user to supply the output stream and must have the write method.
To make the Console class write to file we simply create a file stream:
const fsStream = fs.createWritestream('./log.log')
Our file is log.log
, we created a writable stream to it using fs's createWriteStream API.
We can create another stream we can log our error report:
const errfsStream = fs.createWritestream('./error.log')
We can now pass the two streams to the Console class:
const log = new Console(fsStream, errfsStream)
When we call log.log("logging an input to ./log.log")
, it won't print it to the screen, rather it will write the message to the ./log.log
file in your directory.
Simple, the Console does not have to have a long chain of if/else statement to support any stream.
Angular
Coming to Angular how do we obey the DIP?
Let’s say we have a billing app that lists peoples license and calculates their fees, our app may look like this:
@Component({ template: ` <div> <h3>License</h3> <div *ngFor="let p of people"> <p>Name: {{p.name}}</p> <p>License: {{p.licenseType}}</p> <p>Fee: {{calculateFee(p)}}</p> </div> </div> ` }) export class App { people = [ { name: 'Nnamdi', licenseType: 'personal' }, { name: 'John', licenseType: 'buisness' }, // ... ] constructor(private licenseService: LicenseService) {} calculateLicenseFee(p) { return this.licenseService.calculateFee(p) } }
We have a Service that calculates the fees based on the license:
@Injectable() export class LicenseService { calculateFee(data) { if(data.licenseType == "personal") //... calculate fee based on "personal" licnese type else //... calculate fee based on "buisness" licnese type } }
This Service class violates the DIP, when another license type is introduced we will see ourself adding another if statement branch to support the new addition:
@Injectable() export class LicenseService { calculateFee(data) { if(data.licenseType == "personal") //... calculate fee based on "personal" licnese type else if(data.licenseType == "new license type") //... calculate the fee based on "new license type" license type else //... calculate fee based on "buisness" licnese type } }
To make it obey the DIP, we will create a License interface:
interface License { calcFee(): }
Then we can have classes that implement it like:
class PersonalLicense implements License { calcFee() { //... calculate fee based on "personal" licnese type } // ... other methods and properties }class BuisnessLicense implements License { calcFee() { //... calculate fee based on "buisness" licnese type } // ... other methods and properties }
Then, we will refactor the LicenseService
class:
@Injectable() export class LicenseService { calculateFee(data: License) { return data.calcFee() } }
It accepts data which is a License
type, now we can send any license type to the LicenseService#calculateFee
, it does not care about the type of license, it just knows that the data is a License
type and calls its calcFee
method. It is left for the class that implements the License
interface to provide its license fee calculation in the calcFee
method.
Angular itself also obeys the DIP, in its source. For example in the Pipe concept.
Pipe
Pipe is used to transform data without affecting the source. In array, we transform data like:
-
map
ping -
filter
ing -
sort
ing -
splic
ing -
slic
ing -
substr
ing wink emoji here - etc
All these transform data based on the implementation.
In Angular templates, if we did not have the Pipe interface, we would have classes that transform data pipe
like the Number, Date, JSON or custom pipe, etc. Angular would have its implementation of Pipe like this:
pipe(pipeInstance) { if (pipeInstance.type == 'number') // transform number if(pipeInstance.type == 'date') // transform date }
The list would expand if Angular adds new pipes and it would be more problematic to support custom pipes.
So to Angular created a PipeTransform interface that all pipes would implement:
interface PipeTransform { transform(data: any) }
Now any Pipe would implement the interface and provides its piping function/algorithm in the transform method.
@Pipe(...) class NumberPipe implements PipeTransform { transform(num: any) { // ... } }@Pipe(...) class DatePipe implements PipeTransform { transform(date: any) { // ... } }@Pipe(...) class JsonPipe implements PipeTransform { transform(jsonData: any) { // ... } }
Now, Angular would call the transform without bothering about the type of the Pipe
function pipe(pipeInstance: PipeTransform, data: any) { return pipeInstance.transform(data) }
Conclusion
We saw in this post how DIP makes us write reusable and maintainable code in Angular and in OOP as a whole.
In Engineering Notebook columns for The C++ Report in The Dependency Inversion Principle column, it says:
A piece of software that fulfills its requirements and yet exhibits any or all of the following three traits has a bad design.
- It is hard to change because every change affects too many other parts of the system. (Rigidity)
- When you make a change, unexpected parts of the system break. (Fragility)
- It is hard to reuse in another application because it cannot be disentangled from the current application. (Immobility)
If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me
Thanks !!!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK