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% of any devs work time is spent writing actual code!
Counterintuitive
- planning
- designing
- reading other people's code
- meetings
- scrum
- architecture design
- researching - docs, articles, ..
- requirements gathering
- debugging
- testing
- documentation
- performance reports
- code reviews
✅ Goals
========
1. readable
2. testable
3. maintainable
4. extensible
===================
💎 SOLID Principles
===================
- Single Responsibility
- Open/Close
- Liskov's Substitution
- Interface Segregation
- Dependency Inversion
💭 Context
==========
- Zoo Game 🦊
- characters - animals, zoo staff, visitors
- structures - cages, park, ponds, ..
These concepts will be applicable to any modern language that supports OOP
Python/Java/JS/Typescript/C++/Ruby/C#/Php ..
-----------------------------------------------------------------------------
🎨 Design a Character
======================
```java
class ZooEntity {
// a character inside our zoo game - concept in our mind
// properties (attributes)
// animal
Integer id;
String name;
Integer age;
Integer weight;
Boolean isNonVegetarian;
Color color;
String species;
Gender gender;
// zoo staff
Integer employeeID;
String name;
Integer age;
Gender gender;
String department;
// visitor
Integer ticketID;
String name;
String phone;
String address;
Boolean isVIP;
Gender gender;
// methods (behavior)
// animal
void eat();
void poop();
void sleep();
void attack();
void speak();
void run();
void fly();
// zoo staff
void eat();
void poop();
void speak();
void feedAnimals();
void checkIn();
// visitor
void eat();
void poop();
void getEatenByALion();
void takePhoto();
void speak();
```
Major Issues
1. name collisions for attributes / methods - easy fix - just rename
❓ Readable
Can I read it and understand it? Yes, certainly.
Consider a project with 100,000+ lines of code, 100 devs working on it, 5 year long
project.
Readable right now, but as complexity grows, it will quickly become unreadable.
❓ Testable
I can totally write testcases for each method.
However, because all the code is in a single class, changing the behavior of animal
can (by mistake) end up changing the behavior of the Visitor
❓ Extensible
(we will come back to this later)
❓ Maintainable
dev 1 - features for animals - editing class Zooentity
dev 2 - features for visitors - editing class Zooentity
submit code changes - merge conflicts
==================================
⭐ Single Responsibility Principle
==================================
- if you identify that a piece of code has multiple responsibilities - split the
code into multiple units
```java
// OOP - class Inheritance
class ZooEntity {
// common - age, gender, id, eating, pooping
Integer id;
String name;
Integer age;
Gender gender;
void eat();
void poop();
void speak();
}
void attack();
void run();
void eatAVisitor();
}
void feedAnimals();
void checkIn();
void cleanPremises();
}
/// ...
}
```
- Readable
aren't there wayy too many classes/files now?
yes! earlier we had 1 class. Now we have 4 classes
but that's not an issue!
- because you will never work on multiple features at once.
- at any given time you have to read only 1 or a handful of classes/files
yes we have multiple classes now, but each class is now small and very easy to
understand!
- Testable
Can a change in Animal class effect the behavior of the Staff class?
No!
Responsibilites are now decoupled!
- Maintainable
if multiple devs are working on different files, will it cause merge conflicts?
No / or at least minimized
-----------------------------------------------------------------------------
🐦 Design a Bird
================
```java
// void chirp();
// void poopOnPeopleBelow();
void fly();
}
```
```java
class Bird {
// String species; // inherited from parent class
void fly() {
// how can we implement this?
if (species == "Sparrow")
print("fly low")
else if (species == "eagle")
print("glide elegantly high up above")
else if (species == "pigeon")
print("poop attack anyone below you and fly")
}
}
```
- Readable
- Testable
- Maintainable
- Extensible - FOCUS!
Imagine that I need to add a new Bird type - what code changes will I need to make?
```java
[PublicZooLibary]
{
class Bird {
// String species; // inherited from parent class
void fly() {
// how can we implement this?
if (species == "Sparrow")
print("fly low")
else if (species == "eagle")
print("glide elegantly high up above")
else if (species == "pigeon")
print("poop attack anyone below you and fly")
/**
*/
}
}
}
[MyCustomGame] {
import PublicZooLibary.Bird;
class ZooGame {
void main() {
Bird sparrow = new Bird(...);
sparrow.fly();
```
To add a new type of Bird, I will have to modify the Bird class
- Is it always the case that you have modification access to the source code of
everything that you use?
- Do you always write everything from scratch?
No - you use external libraries
- A lot of libraries are shipped in compiled formats (.dll .com .exe .so .jar
.pyc)
=======================
⭐ Open/Close Principle
=======================
- Code should be closed for modification, yet, it should remain open for extension!
- modification: changing existing code
- you might have to change it for bug fixes
- but you should not change existing code for changing requirements
- extension: adding new functionality in the face of changing requirements
Seems impossible! How can we add new functionality without modifying existing code?
[PublicZooLibary]
{
abstract class Bird {
// String species; // inherited from parent class
[MyCustomGame] {
import PublicZooLibary.Bird;
import PublicZooLibary.Sparrow;
import PublicZooLibary.Eagle;
import PublicZooLibary.Pigeon;
class ZooGame {
void main() {
Sparrow sparrow = new Sparrow(...);
sparrow.fly();
```
- Extension
We were able to extend the Bird functionality (added a new type - Peacock) without
modifying existing code
❔ Isn't this the same thing that we did for Single Responsibility as well?
yes - for SRP we split the class by using inhertiance
for OCP we split the class by using inheritance
❔ Does that mean that OCP == SRP?
No - the solution was the same (OOP) but the intent was different
intent for SRP - was split the responsibilities - readable, testabile, maintainable
intent for OCP - to make the code extensible despite it not being modifiable
🔗 All the SOLID principles are tightly linked together - adhereing to 1 will
automatically make your code good for some others.
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
quick 15 mins break:
resuming at 9.15 PM sharp
-----------------------------------------------------------------------------
- Single Responsibility
- Open/Closed
```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 your problems - simply not implement the `void fly()`
```java
```java
```
```java
class ZooGame {
Bird getBirdObjectFromUserChoice() {
// it shows multiple bird types to the user
// it lets the user select a species
// creates a Bird object of that species
// returns that object
// this function can return a Bird object of type Sparrow, or a Bird object of
type Pigeon
// Runtime polymorphism:
// Sparrow s = new Sparrow();
// return s; // this is allowed, because every sparrow is also a Bird
}
void main() {
Bird b = getBirdObjectFromUserChoice();
b.fly();
}
}
```
✅ Before extension
Code works, easy to understand, passes testcases, everyone is happy
(dev happy, user happy, QA happy)
❌ After extension
```java
class Kiwi extends Bird {
void fly() {
throw new FlightlessBirdException("kiwi's don't fly")
}
}
```
==================================
⭐ Liskov's Substitution Principle
==================================
- Child classes should not violate expectations set by the parent class
- Child classes should not be forced to implement behavior that they can't exhibit
(don't force all Birds to fly, because some of them can't)
```java
class Bird {
// usual stuff
void poop(); // all birds do poop
void eat(); // all birds do eat
// Note: because some birds cannot fly, it will be wrong to expect this from the
Bird class
}
interface ICanFly {
void fly();
}
// flying birds
class Sparrow extends Bird implements ICanFly {
void fly() { print("fly low") }
}
class Pigeon extends Bird implements ICanFly {
void fly() { print("poop attack anyone below you and fly") }
}
// flightless birds
class Kiwi extends Bird {
// No need to implement ICanFly Interface
}
class ZooGame {
ICanFly getFlyingObjectFromUserChoice() {
// it shows multiple bird types to the user
// it lets the user select a species
// creates a Bird object of that species
// returns that object
// this function can return a Bird object of type Sparrow, or a Bird object of
type Pigeon
// Runtime polymorphism:
// Sparrow s = new Sparrow();
// return s; // this is allowed, because every sparrow is also a ICanFly
}
void main() {
ICanFly b = getFlyingObjectFromUserChoice();
b.fly(); // this is perfectly fine!
}
}
```
Q: but Pragy, didn't we modify existing code for this change to happen?
Q: aren't we violating the OCP?
Yes, it would be wrong to start with a bad design and then refactor it into good
design - because then you would be modifying existing code
You want to design the system in a good way right from the very start!
-----------------------------------------------------------------------------
```java
class Bird {
// Note: because some birds cannot fly, it will be wrong to expect this from the
Bird class
}
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]
>
==================================
⭐ Interface Segregation Principle
==================================
note: client =/= user. client == any code that uses your interface
❓ Isn't this similar to LSP? Isn't this just SRP applied to interfaces?
Yes & yes.
But intent is different
Liskov's Substition - type hierarchy (mathematics of the type system in code)
Interface Segregation - writing good code
-----------------------------------------------------------------------------
🗑️ Design a Cage
================
```java
interface IDoor {
void resistAttack(Attack attack); // High Level Abstraction
}
class IronDoor implements IDoor { // Low Level Implementation Detail
void resistAttack(Attack attack) {
if(attack.power <= IRON_MAX_RESISTANCE)
return;
print("Door Broken down - all animals are now dead/escaped")
}
}
class WoodenDoor implements IDoor { ... } // Low Level Implementation Detail
class AdamantiumDoor implements IDoor { ... } // Low Level ...
void feed() {
for(Tiger t: kitties) {
this.bowl.feed(t); // delegating the feeding task to a dependency
}
}
class Cage2 {
// building a cage for chickens
// dependencies - Bowls, Animals, Doors, ...
void feed() {
for(Chicken c: chicks) {
this.bowl.feed(c); // delegating the feeding task to a dependency
}
}
// ...
}
class MyAwesomeZooGame {
void main() {
Cage1 forTigers = new Cage1();
forTigers.feed();
CageXMen ...
}
}
```
High Level
- abstractions: a piece of code that tells you what to do, but not how to do it
(interfaces / abstract classes)
- controllers: manegerial code that delegates tasks to dependencies
Low Level
- implementation details: tell you exactly how something is being done!
```
```
In the above code, the High Level `Cage1` class depends on Low level implementation
details `MeatBowl`, `Tiger`, `IronDoor`
=================================
⭐ Dependency Inversion Principle - what to do
=================================
```
------- --------- -------
IBowl Animal IDoor high level abstractions
------- --------- -------
│ │ │
╰───────────────╁──────────────╯
┃
┏━━━━━━┓
┃ Cage ┃ code
┗━━━━━━┛
```
But how?
=======================
💉 Dependency Injection - how to achieve it
=======================
- Don't create the dependencies yourself - let your clients supply (inject) the
dependencies into you (via constructor/function params)
```java
IBowl bowl;
IDoor door;
List<Animal> animals;
void feed() {
for(Animal a: animals) {
this.bowl.feed(a); // delegating the feeding task to a dependency
}
}
class MyAwesomeZooGame {
void main() {
Cage forTigers = new Cage(
new MeatBowl(),
new IronDoor(),
Arrays.asList(new Tiger("simba"), new Tiger("mufasa")),
);
forTigers.feed();
Enterprise Code
===============
When you go to large companies like Google
- you will find "overengineered code"
That's okay
- because large companies have projects that have
- 100,000+ LOC
- 100s of devs
- 10+ years in pipeline
- devs join & leave all the time
- requirements change frequently
- huge userbases, any mistakes can effect revenue or customer loyalty
Always predict any and all future requirements, and design code from day 1 so that
you don't have to modify it later!
================
🎁 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
>