solid prinicple.

Object-Oriented Programming | Basic concepts of Oops | Oops concepts | Object-Oriented Programming | Solid principles interview questions | Solid principles of Oops | Solid principles javascript | Solid principles in Design Patterns | javascript oops | Liskov substitution principle example | Dependency inversion principle example | Interface segregation principle example | single responsibility principle example | Open-Closed Principle

✔️ Let’s First Understand What is Good Code?

GOOD CODE QUALITY –

1. Readable

2. Testable

3. Extensible

4. Maintainable

5. Scalable (Data Structures & Algorithms & Infrastructure)


SOLID Principles –  Designed by  Robert C. Martin – Uncle Bob (nick name)

Single Responsibility Principle

Open/Close Principle

Liskov’s Substitution Principle

Interface Segregation

Dependency Inversion


💻 Let’s create a General Class structure –

A class contains attributes and behaviors.

class AnimalGeneral {
  // [attributes] properties
  weight: number;
  species: string;
  color: string;
  numerOfLegs: number;
  hasWings: boolean;

  // [Behavior] Methods

  eat() {}
  fly() {}
  swim() {}
  hunt() {}
  run() {}
}


NOTE: In this article, we will understand SOLID principles by examples of animals and their species and their different behaviors.

So SOME SPECIES EXAMPLES ARE –

mammals include rats, cats, dogs, deer, monkeys, apes, bats, whales, dolphins, and humans.
fish – Tuna, salmon, goldfish, and whales are mammals, not fish.
birds – Parrot, Crow, Kite, Eagle, cock
amphibians – frog, wood frog, toad
reptiles – turtle, lizard, crocodiles, snakes


▶️ Now Implement one method in the Animal class.

class Animal {
  species: string;
  // other properties...

  swim() {
    if (this.species === 'fish' || this.species === 'reptiles' || this.species === 'amphibians') {
      console.log(this.species + ' can swim');
    } else if (this.species === 'mammals') {
      console.log('I cannot swim, I am not like a fish!');
    } else {
      console.log('Sorry I cannot swim');
    }
  }
}

▶️ Now Try to write Pseudo Testcases Code for the above Animal Class –

class AnimalTestCases {
  animalObj = new Animal();
  testFishSwim() {
    this.animalObj.species = 'fish';
    this.animalObj.swim();
  }
  testReptilesSwim() {
    this.animalObj.species = 'reptiles';
    this.animalObj.swim();
  }
}


✔️ LET’S MEASURE THE ABOVE CODE QUALITY –

If there are 100 different species, we will need to implement 100 if-else cases. So what!? We have many if-else. Why is that a problem?

Readable Yes. I can read & understand it. But actually no! As the number of species grows, the complexity is growing as well – becomes harder to read and understand.

Testable Seems like it. I can write a test case for each species. However, the test cases and the code is coupled – changing the code for one species will affect the behavior of another.
For example – fish can swim fast than reptiles so if we console.log like ‘fish can swim fast’. This change for just only one species can break other test cases.

Extensible Yes. If a new species get added tomorrow – all I have to do is add a new if-else condition. Will cover more on this later on the same blog. Continue reading…

Maintainable If there are multiple devs working on different species, will there be any issues? Common issue – Merge Conflicts.

 ⭐ Single Responsibility Principle

  1. If some unit of code is serving multiple responsibilities – split it up into multiple units of code.
  2. Every function/class/module should have a simple, well-defined responsibility.
  3. Every unit of code should have exactly 1 reason to change.
class AnimalBase {
  species: string;
  // other properties...

  // This is common method for every animal.
  eat() {}
}

class Bird extends AnimalBase {
  swim() {
    console.log('I am from Bird group, Cannot swim!');
  }
}
class Fish extends AnimalBase {
  swim() {
    console.log('I can swim fast. wooh...');
  }
}
class Mammals extends AnimalBase {
  swim() {
    console.log('I am not like Fish. Sorry!');
  }
}

Now review the above code –

We created different classes based on species and extends them from the animal base class.
Every above-child class has its own swim method and is responsible for it only.
Also If later, you change a message of the Fish class swim method, other test cases will not break.


▶️ But There are problems with the above code

Readable – We have a lot of classes/files now – readability is poor – But Let’s look at individual files I have a lot of units of code, but each unit is individually highly readable!
Testable – Better testability – because changing the behavior of Fish does NOT affect the behavior of others.
Extensible – Seems like no change.
Maintainable – At least the merge conflicts will be reduced.

Object-Oriented Programming | Solid Principles with Examples


🐦 Let’s Desing a Bird Library

// [ Library: Bird ]
class Birds extends AnimalBase {
  // species inherited from the parent class AnimalBase
  bird: string = '';

  fly() {
    if (this.bird === 'peacocks') {
      console.log('I can cover short distances only by flying.');
    } else if (this.bird === 'Penguins') {
      console.log('I am a bird but cannot fly.');
    } else if (this.bird === 'Crow') {
      console.log('I can fly high.');
    } else {
      console.log('No idea..');
    }
  }
}


▶️ Ok, Now how do we use any Library in Client code?

We install packages of libraries that contain compiled code. Now by using the import statement, we import into our components and use its methods and attributes.

import Birds from 'BirdLibrary';

👀 Now Review what is the problem with the above Bird Library which we just created.

 Not extendable –  I want to “extend” this Bird functionality in my code to add a new bird but can not
do because the code is available in the compiled way.

👉 Let’s solve the above problem with the Open close principle.

⭐ Open/Close Principle ⭐

Your code should be closed for modification, however, it should still be open for the extension!


Now we need to understand why should Code not be available for modification.

▶️ Let’s How does the code development cycle work in big companies…

Step 1 – Developer writes code. And test it on your own machine.
Step 2 – Send to QA team for testing. – Manual testing, regression testing, integration testing, etc..
Step 3 – If all testing is well then the deployment cycle starts
Step 4 – Deployment Cycle –
1. Deployed code on a Staging server to test changes.
2. If all is well from 1 then deploy to the Prod server but only for a few users (5% users), not for
everyone.
3. Monitor changes, feedback, and errors for 5% of users. Test code for those.
4. Once all is well, then finally deployed for all users.

 So it’s a very big process to deploy changes, If we modify in existing code. Other teams also use the same library, so modification can break other projects’ functionalities as well.

abstract class Birds1 extends AnimalBase {
  // species inherited from the parent class AnimalBase
  bird: string = '';

  abstract fly(); // This is abstract method so cannot implement this here..
}

Concrete class – A class that extends the abstract class.

class Peacocks extends Birds1 {
  fly(): void {
    console.log('I can cover short distances only by flying.');
  }
}
class Duck extends Birds1 {
  fly(): void {
    console.log('Generally don\'t fly. I found in places where there is water like ponds, rivers');
  }
}

💡Now let’s check the above code –

Modification – Closed for modification.

Extension – Yes, Now we can add the fly method as per our need in the client code.

Didn’t we make the exact same change for both Single Responsibility & Open Close?

Yes, we did! In both cases, we converted the if-else ladder to class inheritance.

❓ Is the SRP == Open/Close Principle?

No. The solution was the same, but the intention was different. The SOLID principles are inherently linked together.

✋ Analogy: if you speak the truth, you’re being honest. If you’re being honest, you’re obviously
speaking the truth.

▶️ Let’s see a problem with the above code –

😒 Kiwi or Penguin cannot fly but still needs to implement the fly method because that is abstract.

class Kiwi extends Birds1 {
  // Concrete class
  fly() {
    // this is unused here..
  }
}

👀 If we don’t implement the fly method, we will get compile error because of the abstract.

 Abstract –  Abstract class says that you can do something (like Bird can fly), ok, but How you can do that one, it needs to tell in child class.

💻 Let’s throw some exceptions from the fly method –

class Kiwi1 extends Birds1 {
  // Concreate class
  fly() {
    throw new Error('Kiwi dont fly'); // throw exception.
  }
}

💻 Let’s Create a Main class that uses the Bird class –

class Main {
  main() {
    var birdObj = new Birds();
    birdObj.bird = 'peacocks'; // it is User choice.
    birdObj.fly();
    birdObj.bird = 'kiwi';
    birdObj.fly();
  }
}

// Run class
const mainObj = new Main();
mainObj.main();

// [LOG]: "I can cover short distances only by flying."
// [LOG]: "No idea.."

Before the extension, the above code is working fine as we have all If Else in one place. But after extension, Kiwi throws an error so the code will get an error. It could be the case that throwing an exception could break someone’s code.

Object-Oriented Programming | Solid Principles with Examples

▶️ Let’s solve the above problem of exception and error –

⭐ Liskov Substitution Principle ⭐

1. Any functionality in the parent class must also work for the child class.

2. If some piece of code works with a parent class P, then it should continue working without modifications, with any child class C extending P.

3. Any extension to existing code should not break existing code / violate expectations.

🎨 How should we re-design it?

abstract class Bird2 extends AnimalBase {
  // species inherited from AnimalBase
  bird: string = '';

  // every bird can speak or eat so put these method here..
  speak() {}
  eat() {}

  // don'\t define abstract method here like fly or swim etc...
}

💡 Create an Interface for Fly

interface IFly {
  fly(): void;
}

Now We know Eagle can fly so implement the IFly interface. & As the Dodo bird cannot fly so no need to implement the IFly interface here.

class Eagle extends Bird2 implements IFly {
  fly() {
    // fly
  }
}

class Dudo extends Bird2 {}

💡 Let’s create another interface ICanFly

interface ICanFly {
  flapWings(): void;
  kickOffGroundToTakeOff(): void;
  fly(): void;
}
class Shaktiman implements ICanFly {
  flapWings() {} // Shaktiman dont have wings so this method is unused here
  fly() {
    console.log('With one hand up, Shaktimaan spines & then fly away');
  }
  kickOffGroundToTakeOff() {}
}
class Airplane implements ICanFly {
  flapWings(): void {} // unused method implementation because Airplane does not flap wings
  fly(): void {
    console.log('I can fly');
  }
  kickOffGroundToTakeOff(): void {
    console.log('ready for take off..');
  }
}

* PROBLEM IN THE ABOVE CODE –

Classes implement interfaces that’s why It is required to implement all its methods whether the class needs it or not.

⭐ Interface Segregation Principle ⭐

1. Keep your interfaces minimal.

2. No code should be forced to implement a method that it does not need.

✔️ To fix the previous code, we can split the ICanFly interface into multiple interfaces ICanFly, IHasWings, and IHasLegs.

Object-Oriented Programming | Basic concepts of Oops | Oops concepts | Object-Oriented Programming | Solid principles interview questions | Solid principles of Oops | Solid principles javascript | Solid principles in Design Patterns | javascript oops | Liskov substitution principle example | Dependency inversion principle example | Interface segregation principle example | single responsibility principle example | Open-Closed Principle

Leave a Reply

Your email address will not be published.