SOLID Principles
SOLID Principles
=======================
Pragy
Senior Software Engineer + Instructor @ Scaler
https://linktr.ee/agarwal.pragy
💎 Key Takeaways
================
❓ FAQ
======
▶️Will the recording be available?
To Scaler students only
🎧 Audio/Video issues
Disable Ad Blockers & VPN. Check your internet. Rejoin the session.
💡 Prerequisites?
Basics of Object Oriented Programming
Important Points
================
>
> ❓ What % of your work time is spend writing new code?
>
> • 10-15% • 15-40% • 40-80% • > 80%
>
< 15%
✅ Goals
========
1. Readable
2. Extensible
3. Maintainable
4. Testable
===================
💎 SOLID Principles
===================
-----------------------------------------------------------------------------
🎨 Design a Character
======================
```java
class ZooEntity {
// attributes (properties)
// staff
String visitorAddress;
String visitorName;
Integer age;
Gender gender;
Boolean isVegetarian;
// animals
String name;
Integer age;
Gender gender;
Boolean isVegetarian;
Boolean canFly;
String species;
// visitors
String address;
String name;
String ticketID;
Gender gender;
// methods (behavior)
// staff
void checkInToWork();
void feedAnimals();
void poop();
void eat();
void getPaid();
// animals
void eat();
void poop();
void fly();
void swim();
void attackVisitors();
// visitors
void eat();
void litter();
void buyTicket();
```
Major Issues: name collisions! So, I need to rename the attributes and
methods. Simple fix
❓ Readable
This code looks readable - I can totally read and understand it!
(for now)
But as the complexity and requirements grow, the code will very quickly
become unreadable.
There's a lot of code in a single class - if I wish to understand how this
class works, I need to read all these (potentially) thousands of lines of
code
❓ Testable
I can totally write testcases for each individual method
```java
class ZooEntityTester {
void testAnimalEat() {
...
}
}
```
Behavior of staff, animals, visitors is thightly coupled - because everything
is in 1 class.
Changing 1 can adversely effect the other.
Therefore this code is hard to test - any changes can have unintended side
effects
❓ Extensible
(will come back to this later)
❓ Maintainable
If there are 3 devs - each working on 1 type of character (animal, staff,
visitor)
All these 3 devs are modifying the same class/file
PR - merge conflicts
Difficult to maintain
- another way to state it: any piece of code should have only 1 reason to
change
```java
// make use of inheritance (from OOP)
void eat();
void poop();
void speak();
}
void attackVisitors();
void fly();
}
void litter();
void registerForNightShow();
}
void cleanPremises();
void feedAnimals();
}
```
❓ Readable
We have way too many classes now!
Earlier: 1 class Now: 4 classes
❓ Testable
Can any changes made to `class Animal` effect the behavior of `class Staff`?
No!
Thus, the code is decoupled - it is easier to test (lesser side effects)
❓ Extensible
(will come back to this later)
❓ Maintainable
Merge conflict - if diff devs are working on different features - most likely
they will be working on diff classes (files)
Less merge conflicts
-----------------------------------------------------------------------------
🐦 Design a Bird
================
```java
class Animal extends ZooEntity {
String species;
}
class Bird extends Animal {
// species is inherited by the class Bird
Integer beakLength;
Integer wingSpan;
void fly() {
print("flap wings")
}
}
```
```java
class Bird extends Animal {
// species is inherited by the class Bird
void fly() {
if (species == "pigeon")
print("flap wings, make noise, poop on people below, fly")
else if (species == "eagle")
print("spread wings and glide elegantly")
else if (species == "penguin")
print("bro I can't fly")
}
}
```
- Readable
- Testable
- Maintainable
- Extensible - FOCUS!
Extensibility = being able to add new features/functionality "easily"
I will need to modify the `class Bird` and add a new else-if condition inside
it
```java
// class Bird extends Animal {
// // species is inherited by the class Bird
// void fly() {
// if (species == "pigeon")
// print("flap wings, make noise, poop on people below, fly")
// else if (species == "eagle")
// print("spread wings and glide elegantly")
// else if (species == "penguin")
// print("bro I can't fly")
else if(species == "peacock")
print("female (pehens) can fly, but the males cannot")
// }
// }
```
```java
// now use it
class MyZooGame {
static void main() {
Bird b = new Bird(...);
b.fly();
}
}
}
```
❓ How can we add a new type of Bird?
You still need to modify the Bird class to add peacock
========================
⭐ Open/Closed Principle
========================
- your code should be closed for modification, but still, remain open to
extension!
- modification: changing existing code
- extension: not changing existing code, but adding new code in a separate
file
Seems impossible! How on earth can we add new features if we cannot modify
existing code!?
If a piece of code has already gone through this extensive process, you do
NOT want to repeat this
Only new code should go through this process
```java
[MyCustomGame] {
// first import the external library
import PublicZooLibary.Bird;
import PublicZooLibary.Pigeon;
import PublicZooLibary.Eagle;
import PublicZooLibary.Penguin;
```
- Extension
If I need to add a new type of Bird what should I do?
I just just implement another class
❔ Isn't this the same thing that we did for Single Responsibility as well?
Yes! In both cases, we split the large class into multiple smaller classes!
🔗 All the SOLID principles are linked to one another. They come as a
package.
-----------------------------------------------------------------------------
Projects
-----------------------------------------------------------------------------
9:04 PM - 9:20 PM (15 mins break)
-----------------------------------------------------------------------------
```java
class ZooEntity {
...
}
```
```java
>
> ❓ How do we solve this?
>
> • Throw exception with a proper message
> • Don't implement the `fly()` method
> • Return `null`
> • Redesign the system
>
🏃♀️ Run away from the problem - simply don't implement the `void fly()` method
```java
void poop() {
...
}
}
```
🐞 Violates expectations!
✅ Before extension
Code works
Properly tested
devs are happy
users are happy
```java
abstract class Bird extends Animal {
abstract void fly();
}
class Main {
Bird getBirdTypeFromUserChoice() {
// shows the different types of birds to the user
// lets the user pick one class
// creates an object of that class
// and returns that object
if(userChoice == "pigeon")
return new Pigeon(...)
else if (...)
return new ...
...
// instead of if-else, use factory pattern
}
void main() {
Bird b = getBirdTypeFromUserChoice();
b.fly();
}
}
```
❌ After extension
The code doesn't work!
```java
class Kiwi extends Bird {
void fly() {
throw new FlightlessBirdException("bro, Kiwi's don't fly")
}
}
```
==================================
⭐ Liskov's Substitution Principle
==================================
Barbara Liskov
- Any object of a `class Child extends Parent` should be able to replace any
object of a `class Parent` without any issues.
- anywhere you expect to see a parent class object, you should be able to
use a child class object without breaking anything!
```java
/**
* INCORRECT
abstract class Bird extends Animal {
abstract void fly(); // enforcing that all Birds should be able to fly
}
*/
class Bird {
// not all birds can fly
// so we will not enforce any void fly here
interface ICanFly {
void fly();
}
class Main {
Bird getBirdTypeFromUserChoice() {
...
}
void main() {
Bird b = getBirdTypeFromUserChoice();
```py
from abc import ABC, abstractmethod
class Bird(ABC):
...
class FlightMixin(ABC):
@abstractmethod
def fly(self):
raise NotImplementedError
class Kiwi(Bird):
# no need to implement fly() method
...
```
-----------------------------------------------------------------------------
```java
class Bird {
...
}
interface ICanFly {
void fly();
void flapWings();
}
void flapWings() {
// SORRY Shaktiman!
}
}
```
>
> ❓ Should these additional methods be part of the ICanFly interface?
>
> • Yes, obviously. All things methods are related to flying
> • Nope. [send your reason in the chat]
>
No - these should NOT be part of ICanFly interface, because not all the
things that can fly can support these
==================================
⭐ Interface Segregation Principle
==================================
❓ Isn't this similar to LSP? Isn't this just SRP applied to interfaces?
SRP vs ISP => ISP can apply to APIs as well, not just classes
🔗 All SOLID principles are tightly linked to each other. You see view them
together, not as individual principles
-----------------------------------------------------------------------------
🗑️ Design a Cage
================
```java
abstract class Animal { ... } // High Level Abstraction
class Tiger extends Animal { ... } // Low Level Implementation
class Doggy extends Animal { ... } // Low Level Implementation
class Bird extends Animal { ... } // Low Level Implementation
// dependencies
IronDoor door = new IronDoor();
MeatBowl bowl = new MeatBowl();
public Cage1() {}
public feed() {
// delegate the task to the dependency // delegates tasks
for(Tiger t: this.kitties)
this.bowl.feed(t)
}
// dependencies
WoodenDoor door = new WoodenDoor();
FruitBowl bowl = new FruitBowl();
public Cage2() {}
public feed() {
// delegate the task to the dependency
for(Bird b: this.birdies)
this.bowl.feed(b)
}
class Game {
void main() {
Cage1 tigerCage = new Cage1();
...
Cage2 birdCage = new Cage2();
...
}
}
class Cage3 {
// for xmen
// ...
}
```
```
In the above code, the class `Cage1` depends on low level implementation
details `MeatBowl`, `Tiger`, `IronDoor`
=================================
⭐ Dependency Inversion Principle - what do achieve
=================================
- any code (high/low level) should NEVER depend on low level implementation
details
- should ONLY depend on high level abstractions
```
------- --------- -------
IBowl Animal IDoor High level abstractions
------- --------- -------
│ │ │
╰───────────────╁──────────────╯
┃
┏━━━━━━┓
┃ Cage ┃ Code
┗━━━━━━┛
```
But how?
=======================
💉 Dependency Injection - how to achieve it
=======================
```java
interface IDoor { ... } // High Level Abstraction
class IronDoor implements IDoor { ... } // Low Level Implementation Detail
class WoodenDoor implements IDoor { ... } // Low Level Implementation Detail
class AdamantiumDoor implements IDoor { ... } // Low Level ...
// dependencies
IDoor door;
IBowl bowl;
List<Animal> animals;
public feed() {
// delegate the task to the dependency // delegates tasks
for(Animal a: this.kitties)
this.bowl.feed(a)
}
class Game {
void main() {
Cage tigerCage = new Cage(
new IronDoor(...),
new MeatBowl(...),
Arrays.asList(new Tiger(...), new Tiger(...))
);
Cage birdCage = new Cage(
new WoodenDoor(...),
new FruitBowl(...),
Arrays.asList(new Eagle(...), new Peacock(...))
);
```
All the fancy frameworks use dependency inversion & injection! They also use
SOLID principles
- backend
- Rails (ruby)
- Spring (java)
- Django / Flask / FastAPI (python)
- Laravel (php)
- .Net (C#, F#, ...)
- Express (Nodejs)
- frontend
- React
- Vue
- Angular
- Svelte
- Fullstack
- Dart + Flutter
Enterprise Code
===============
Design Patterns / Principle / .. all these are heavily hammered into every
piece of code
```java
class StripePaymentGatewayServiceStrategyFactory implement
IPaymentGatewayServiceStrategyFactory {
StripePaymentGatewayServiceStrategyFactory instance;
================
🎁 Bonus Content
================
>
> We all need people who will give us feedback.
> That’s how we improve. 💬 Bill Gates
>
-------------
🧩 Assignment
-------------
https://github.com/kshitijmishra23/low-level-design-
concepts/tree/master/src/oops/SOLID/
----------------------
⭐ Interview Questions
----------------------
>
> ❓ Which of the following is an example of breaking
> Liskov Substitution Principle?
>
> A) A subclass that overrides a method of its superclass and changes
> its signature
>
> B) A subclass that adds new methods
>
> C) A subclass that can be used in place of its superclass without
> any issues
>
> D) A subclass that can be reused without any issues
>
> ❓ How can we achieve the Interface Segregation Principle in our classes?
>
> A) By creating multiple interfaces for different groups of clients
> B) By creating one large interface for all clients
> C) By creating one small interface for all clients
> D) By creating one interface for each class
> ❓ Which SOLID principle states that a subclass should be able to replace
> its superclass without altering the correctness of the program?
>
> A) Single Responsibility Principle
> B) Open-Close Principle
> C) Liskov Substitution Principle
> D) Interface Segregation Principle
>
>
> ❓ How can we achieve the Open-Close Principle in our classes?
>
> A) By using inheritance
> B) By using composition
> C) By using polymorphism
> D) All of the above
>