DEV Community

Chidume Nnamdi
Chidume Nnamdi

Posted on • Updated on

SOLID: Dependency Inversion Principle in Angular

Post originally published at blog.bitsrc.io

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.
Share reusable code components as a team · Bit
*Easily share reusable components between projects and applications to build faster as a team. Collaborate to develop…*bit.dev

What are Abstractions?

Abstractions are Interfaces. Interfaces define what implementing Classes must-have. If we have an Interface Meal:

interface Meal {
    type: string
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Now, our disk reader would implement the Reader interface:

class DiskReader implements Reader {
    read(bytes) {
        //.. implementation here
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

The Copy function would be like this:

function Copy(to) {
    let bytes = []
    while(ReadFromDisk(bytes))
        if(to == To.Net)
            WriteToNet(bytes)
        else
            WriteToFlashDrv(bytes)
}


|
|
v

function Copy(writer: Writer, reader: Reader) {
    let bytes = []
    while(reader.read(bytes))
        writer.write(bytes)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

We can now pass the two streams to the Console class:

const log = new Console(fsStream, errfsStream)
Enter fullscreen mode Exit fullscreen mode

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)        
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

To make it obey the DIP, we will create a License interface:

interface License {
    calcFee():
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Then, we will refactor the LicenseService class:

@Injectable()
export class LicenseService {
    calculateFee(data: License) {
        return data.calcFee()
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  • mapping

  • filtering

  • sorting

  • splicing

  • slicing

  • substring 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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, Angular would call the transform without bothering about the type of the Pipe

function pipe(pipeInstance: PipeTransform, data: any) {
return pipeInstance.transform(data)
}
Enter fullscreen mode Exit fullscreen mode




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.

  1. It is hard to change because every change affects too many other parts of the system. (Rigidity)

  2. When you make a change, unexpected parts of the system break. (Fragility)

  3. 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 !!!

Learn More

How to Share Angular Components Between Projects and Apps
*Share and collaborate on NG components across projects, to build your apps faster.*blog.bitsrc.io

Announcing Bit with Angular Public Beta
*Special thanks to the awesome Angular team for working together on making this happen 👐*blog.bitsrc.io

Top comments (0)