0% found this document useful (0 votes)
4 views

SOLID Principles

Uploaded by

nojam49073
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

SOLID Principles

Uploaded by

nojam49073
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 22

💎 Key Takeaways

================

✅ In-depth understanding of SOLID principles


✅ Walk-throughs with examples
✅ Practice quizzes & assignment

❓ FAQ
======
▶️Will the recording be available?
To Scaler students only

✏️Will these notes be available?


Yes. Published in the discord/telegram groups (link pinned in chat)

⏱️ Timings for this session?


5pm - 8pm (3 hours) [15 min break midway]

🎧 Audio/Video issues
Disable Ad Blockers & VPN. Check your internet. Rejoin the session.

❔ Will Design Patterns, topic x/y/z be covered?


In upcoming masterclasses. Not in today's session.
Enroll for upcoming Masterclasses @ [scaler.com/events]
(https://www.scaler.com/events)

🖥️ What programming language will be used?


The session will be language agnostic. I will write code in Java.
However, the concepts discussed will be applicable across languages

💡 Prerequisites?
Basics of Object Oriented Programming

👨‍💻About the Instructor


=======================

Pragy
Senior Software Engineer + Instructor @ Scaler
https://linktr.ee/agarwal.pragy

Important Points
================

💬 Communicate using the chat box

🙋 Post questions in the "Questions" tab

💙 Upvote others' question to increase visibility

👍 Use the thumbs-up/down buttons for continous feedback

⏱️ Bonus content at the end


-----------------------------------------------------------------------------

>
> ❓ What % of your work time is spend writing new code?
>
> • 10-15% • 15-40% • 40-80% • > 80%
>

We spend < 15% of our time as devs to write new code!

⏱️ Where does the rest of the time go?

- learning
- brainstorming
- reading code / docs
- maintainable
- KT

- meetings

- breaks - chai, TT (I want to max my time here)

Whatever work I do, it has to be very high quality.


How do we ensure that our code is good quality?

✅ Goals
========

We'd like to make our code

1. Readability
2. Extensibility
3. Maintainability
4. Testability

#### Robert C. Martin 🧓🏻 Uncle Bob

===================
💎 SOLID Principles
===================

- Single Responsibility
- Open/Close
- Liskov's Substitution
- Interface Segregation
- Dependency Inversion
Interface Segregation / Inversion of Control
Dependency Inversion / Dependency Injection

This class will follow "java-like syntax" - not exactly java - it will be pseudo-
code
Any principles that we discuss here will be applicable in any programming language
that supports Object Oriented Programming

💭 Context
==========

- Zoo game 🦊
- characters - Animals, Staff, Visitors
- structures - Gardens, Roads & Paths, Cages

-----------------------------------------------------------------------------

🎨 Designing the Characters


============================

```java

/**
* Concept (in mind) - Class (in code)
* Attributes (in mind) - properties (in code)
* Behavior (in mind) - methods (in code)
*/

class ZooEntity {
// it abstracts all the various characters present in the zoo

// properties [attributes]

// zoo staff
String name; // rename - staffName
Integer age;
Integer employeeId;
String phoneNumber;
String address;

// visitors
String name; // rename - visitorName
Integer age;
Ticket ticket;
String phoneNumber;

// animals
String species;
String name;
Integer age;
Integer weight;
Boolean canFly;

// methods [behavior]

// staff
void clean() { ... }
void eat() { ... }
void poop() { ... }
void sleep() { ... }
void feedAnimals() { ... }

// visitor
void eat() { ... }
void poop() { ... }
void feedAnimals() { ... }
void getEatenByLion() { ... }

// animals
void eat() { ... }
void poop() { ... }
void sleep() { ... }
void attackVisitors() { ... }

```

🐞 Problems with the above code?

❓ Readable

It does look readable - I can totally read it and understand it.


Imagine if I expand into the types of staff, or types of visitors / animals.
Name collisions / class is too complex / there's just too much going on

As the codebase becomes larger and the features grow, this will quickly get out of
hands!

❓ Testable

Yes, I can write testcases.


Code is tightly coupled - making changes in behavior of Staff might cause
unintentional changes in the behavior of Animals.
This makes testing really hard!

❓ Extensible
will come back to this

❓ Maintainable
what happens when instead of 1 dev you have 100 devs on the team?
Because everyone is working on the same class, then they finally commit the code -
we will get merge conflicts!

🛠️ How to fix this?


==================================
⭐ Single Responsibility Principle
==================================

- Any class/function/module (unit-of-code) should have exactly 1, and well defined,


responsibility

- Any piece of code should have only 1 reason to change

- If some piece of code has multiple responsibilities, then we should - split it


into multiple parts

```java

abstract class ZooEntity {


String name;
Integer age;

abstract void eat();


abstract void poop();
}

class Animal extends ZooEntity {


void attackVisitors();
// ...
}

class ZooStaff extends ZooEntity {


void clean();
// ...
}

class Visitor extends ZooEntity {


void feedAnimals();
// ...
}

```

- Readable
Now we have too many classes (earlier - 1, now - 4)
Lots of boilerplate `class X extends Y`

At any given time, you will have to work with 1 (or maybe a handful) of features - a
handful of classes
Each class is now very simple to understand - small, and it makes sense!

- Testable
If I make changes in the Animal class, will that effect the behavior of the Visitor
class?
No!
Testcases will now be more robust! Because the code is decoupled!

- Maintainable
If diff devs are working on different classes - what happens to the merge conflicts
Goes down!
-----------------------------------------------------------------------------
Prerequisite - Object Oriented Programming
-----------------------------------------------------------------------------

🐦 Design a Bird
================

```java
class Bird extends Animal {

// inherits the attribute String species from parent class

void fly() {
// Different birds fly differently!

if (species == "sparrow")
print("fly low")
else if (species == "eagle")
print("glide high in the sky")
else if (species == "peacock")
print("females can fly, males cant")
}

void chirp() { ... }


void buildNest() { ... }

}
```

🐞 Problems with the above code?

- Readable
- Testable
- Maintainable

- Extensible - FOCUS!

Do we write all our code from scratch? Or do we sometimes import external libraries?

```java

[external library] SimpleZooLibrary


class Animal { ... }

class Bird extends Animal {

// inherits the attribute String species from parent class

void fly() {
// Different birds fly differently!

if (species == "sparrow")
print("fly low")
else if (species == "eagle")
print("glide high in the sky")
else if (species == "peacock")
print("females can fly, males cant")

// else if (species == "kite")


// print(...)
}

void chirp() { ... }


void buildNest() { ... }
}

[our code] MyZooGame


import SimpleZooLibrary.Animal;
import SimpleZooLibrary.Bird;

// I wish to add a new type of bird here


// I want to add Kite
// how can I do that?

// I canNOT!

class App {
void main() {
Bird sparrow = new Bird(...);
sparrow.fly();

Bird eagle = new Bird(...);


eagle.fly();
}
}
```

Do you always have the source code for external libraries?


No - mostly you don't
- library is shipped in compiled format
- dll, exe, com, so, jar, pyc, ...
Sometimes you do - if the library is open source
- even then, you don't have modification rights to the code

Code is not extensible, especially when imported from an external library

Zerodha - Kite developer API


Write your custom trading strategies & lose money on the market
You (the user) wants to be able to write your custom strategies - extend the
functionality of the code that Zerodha wrote

🛠️ How to fix this?

=======================
⭐ Open/Close Principle
=======================

- Code should be closed for modification, yet, open for extension!

Seems impossible! how can I extend (add functionality) to code, without modifying
it?
Don't modify "existing" code which is already working

❔ Why is modifying existing & working code bad?


Lifecycle of code
- dev codes on local machine. Test it, update docs, commit & push
- Pull Request (PR). PR review. Feedback. Iterate (few days)
- Quality Assurance (QA) team - write more tests (few days)
- Deployment Life Cycle
+ Staging servers (a day)
+ monitor if something is going wrong
+ check if the process completes successfully
+ Production servers
* A/B testing (a week)
- deploy to only 5% of userbase
+ monitor # of exception
+ monitor rating & user satisfaction
+ monitor revenue
* finally deployed to 100% of the userbase

this all takes several weeks

If some piece of code has already gone through all this crap, then you don't wanna
repeat it.

But we still want to extend it! We want to add new features!

```java

[external library] SimpleZooLibrary


class Animal { ... }

abstract class Bird extends Animal {

// inherits the attribute String species from parent class

abstract void fly();

void chirp() { ... }


void buildNest() { ... }
}

class Sparrow extends Bird {


void fly() {
print("fly low")
}
}

class Eagle extends Bird {


void fly() {
print("glide high in the sky")
}
}

class Peacock extends Bird {


void fly() {
print("females can fly, males cant")
}
}

[our code] MyZooGame


import SimpleZooLibrary.Animal;
import SimpleZooLibrary.Bird;
// I wish to add a new type of bird here
// I want to add Kite
// how can I do that?

class Kite extends Bird {


void fly() {
print("custom behavior")
}
}

class App {
void main() {
Sparrow sparrow = new Sparrow(...);
sparrow.fly();

Eagle eagle = new Eagle(...);


eagle.fly();
}
}

```

- Extension
Did we modify existing code?
No! We did not even have the source code
But, did we add new functionlaity?
Yes!

Didn't we modify the ZooLibrary code?


No!
If you followed the SOLID principles from the start, then the library would look
like the 2nd version from the start.
Other devs (your users) will be able to extend, without you having to modify

As devs, we should write our code in a manner that allows others to extend it
without modifying it.

Top engineer salaries in tier-1 companies in India


Google - Staff Engineer / Principle Engineer
Benagaluru / Hyderabad

3 crores (base pay)

Why would the comapnies pay so much - what do these devs do differently?
These people are expected to anticipate future requirement changes, and write code
that is extensible from day 1.

(design patterns)

❔ Isn't this the same thing that we did for Single Responsibility as well?
In both cases, we simply broke the large class down into smaller classes

❔ Does that mean that OCP == SRP?


No. The solution was same, but the intent & effect was different.

🔗 All the SOLID principles are tightly linked together. Adhering to one might give
you others for free!
-----------------------------------------------------------------------------

Data Structures & Algorithm


Databases & SQL
Concurrency & Multithreading
Computer Networks
Operating Systems

Low Level Design


----------------
1. Object Oriented Programming
2. SOLID principles
3. Design Patterns
- creational
- structural
- bheavioral
4. Case studies
- Snake Ladder / Chess / ..
- Library Management
- Parking Lot
- BookMyShow
5. Design Database Schema
6. ER diagrams
7. API design

Projects
High Level Design

Builder pattern in Python


Truth = you should NEVER use the builder pattern in Python!
Point - choose your study resources carefully, because there's alot of false
knowledge online

-----------------------------------------------------------------------------

8.56 - 9.10 sharp

-----------------------------------------------------------------------------

🐓 Can all the birds fly?


=========================

```java

abstract class Bird extends Animal {


abstract void fly();
}

class Sparrow extends Bird { void fly() { print("fly low") }}

class Eagle extends Bird { void fly() { print("glide high in the sky") } }

class Kiwi extends Bird {


void fly() {
// what should we do here?
}
}

```

Are there certain birds that cannot fly?


Penguins, Ostrich, Kiwi, Dodos, Emu, ..

>
> ❓ How do we solve this?
>
> • Throw exception with a proper message
> • Don't implement the `fly()` method
> • Return `null`
> • Redesign the system
>

🏃‍♀️ Run away - Simply don't implement the void fly!

```java

abstract class Bird extends Animal {


abstract void fly(); // this part is incomplete
}

class Kiwi extends Bird {


// no void fly

void chirp() { ... }


void eat() { ... }
}

```

🐞 Compiler Error!

Abstract class = incomplete blueprint


We're telling the compiler that this class definition is incomplete - you cannot
create objects of this class directly
(abstract classes cannot be instantiated)

Anyone who inherits from this class, either they should provide the void fly, or
they should themselves be marked abstract

⚠️Throw an exception - with a proper message / return null

Return null is bad - surpressing the issue silently (silent error)


throw a detailed & proper exception

```java

abstract class Bird extends Animal {


abstract void fly(); // this part is incomplete
}
class Sparrow extends Bird { void fly() { print("fly low") }}

class Eagle extends Bird { void fly() { print("glide high in the sky") } }

class Kiwi extends Bird {


void fly() {
throw new FlightlessBirdException("bro, Kiwi's can't fly!")
}
}

```

🐞 Violates expectations!

```java

abstract class Bird extends Animal {


abstract void fly(); // this part is incomplete
}

class Sparrow extends Bird { void fly() { print("fly low") }}

class Eagle extends Bird { void fly() { print("glide high in the sky") } }

class ZooGame {

Bird getBirdFromUserChoice() {
// shows all the various bird species to user
// user chooses one
// creates an object for that species
// returns that object
if(choice == "sparrow")
return new Sparrow("tweety")
}

void main() {
Bird birdie = getBirdFromUserChoice();
birdie.fly();
}

```

✅ Before extension
- code is working fine
- well tested
- working
- dev is happy, and the user is happy

❌ After extension

Someone added the below code


```java
// intern tasked with adding a new Bird kiwi
class Kiwi extends Bird {
void fly() {
throw new FlightlessBirdException("bro, Kiwi's can't fly!")
}
}
```
Did we change existing code? No
Was the old code working earlier? Yes
Is the new code correct? Yes, looks like it
Does the system continue to work correctly? NOPE!

==================================
⭐ Liskov's Substitution Principle
==================================

- gist: don't violate expectations set by your parents

- any place where you expect an object of `class Parent`, you should be able to use
an object of `class Child extends Parent` without running into any issues!

- a parent class object should be replacable by a child class object

- any child class should support all functionality of the parent class
- it can add additional functionality of its own
- but it should support at least whatever the parent supports
- otherwise, you need to redesign

🎨 Redesign the system!

```java

abstract class Bird extends Animal {


// since we know that not all birds can fly
// we should NOT have the void fly here!
}

interface ICanFly {
void fly();
}

class Sparrow extends Bird implements ICanFly { void fly() { print("fly low") }}

class Eagle extends Bird implements ICanFly { void fly() { print("glide high in the
sky") } }

class Kiwi extends Bird {


// we don't have to implement ICanFly, and therefore, we don't need
// to provide the void fly() method!
}

class ZooGame {

ICanFly getBirdFromUserChoice() {
// shows all the various bird species to user
// user chooses one
// creates an object for that species
// returns that object
if(choice == "sparrow")
return new Sparrow("tweety")
}

void main() {
ICanFly birdie = getBirdFromUserChoice();
birdie.fly(); // this will NEVER violate expectations!
}
}

```

```py

from abc import ABC, abstractmethod

class Bird(ABC):
def chirp(self):
...

class ICanFly(ABC):
@abstractmethod
def fly(self):
raise NotImplementedError

class Sparrow(Bird, ICanFly):


def fly(self):
...

class Kiwi(Bird):
...
# no need of fly() method here

```

-----------------------------------------------------------------------------

✈️What else can fly?


=====================

```java

abstract class Bird extends Animal {


// since we know that not all birds can fly
// we should NOT have the void fly here!
}

interface ICanFly {
void flapWings();
void kickToTakeOff();

void fly();

// to fly, they will first kick off from the ground


// then they will spread their wings
// and then they will start flying
}

class Sparrow extends Bird implements ICanFly { void fly() { ... }}


class Eagle extends Bird implements ICanFly { void fly() { ... } }
class Kiwi extends Bird {...}

class Shaktiman implements ICanFly {

void flapWings() {
SORRY Shaktiman!
}

```

Aeroplanes, Helicopters, Shaktiman, Drones, Mom's Chappal, Patang, ..

>
> ❓ 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. They should not be part of ICanFly, if it is possible for things that can fly,
but don't have wings!

==================================
⭐ Interface Segregation Principle
==================================

- keep your interfaces minimal

- your client (another piece of code that is using your class/interface) should not
be forced to implement functionality that it does not support

How will you fix `ICanFly`?

Split it into multiple interfaces


`ICanFly`, `IHasWings`

```java
abstract class Bird {}

interface ICanFly {
void fly()
}

interface IHasWings {
void flapWings()
}

class Sparrow extends Bird implements ICanFly, IHasWings {


void fly() { ... }
void flapWings() { ... }
}

class Shaktiman implements ICanFly {


void fly() { print("put finger up and rotate") }

// he doesn't have to spread his wings!


}
```

❓ Isn't this just the SRP applied to interfaces?


Yes, it is, but it is important because this is not about code complexity - it is
about what your clients have to do

🔗 All SOLID principles are linked together!

-----------------------------------------------------------------------------

Premature optimization is the root of all evil!

-----------------------------------------------------------------------------

🗑️ Design a Cage
================

```java

/*

High Level
abstractions
code that doesn't tell you how to do things
just tells you what needs to be done

controllers
code that delegates the task forward to low level implementation details
acts as manager

Low Level
implementation details
code that tell you exactly how to perform certain task

*/

// first create certain dependencies


// dependencies = anything that other code depends on

interface IBowl { // high-level abstraction


void feed(Animal animal);
}
class FruitBowl implements IBowl { ... } // low-level implementation detail
class MeatBowl implements IBowl { // low-level implementation detail
void feed(Animal animal) {
// measure and cut pieces
// add digestive enzymes
// add vitamins
// carefully place the food in bowl
// monitor animal to ensure it doesn't choke
}
}
class GrainBowl implements IBowl { ... } // low-level implementation detail
interface IDoor { // high-level abstraction
void resistAttack(Attack attack);
}
class WoodenDoor implements IDoor { ... } // low-level implementation detail
class IronDoor implements IDoor { ... } // low-level implementation detail
class AdamantiumDoor implements IDoor { ... } // low-level ...

class Cage1 { // high-level controller


// create a cage for tigers

MeatBowl bowl = new MeatBowl(); // I made these dependencies


IronDoor door = new IronDoor();
List<Tiger> tigers = new ArrayList();

public Cage1 {
this.tigers.add(new Tiger(...));
this.tigers.add(new Tiger(...));
}

void feed() {
for(Tiger t: this.tigers)
this.bowl.feed(t) // delegate the task to the bowl
}

void resistAttack(Attack attack) {


this.door.resistAttack(attack) // delegate the task to the door
}
}

class Cage2 { // high-level controller


// create a cage for birds

FruitBowl bowl = new FruitBowl();


WoodenDoor door = new WoodenDoor();
List<Bird> birds = new ArrayList();

public Cage2 {
this.birds.add(new Sparrow(...));
this.birds.add(new Pigeon(...));
}

void feed() {
for(Bird t: this.birds)
this.bowl.feed(t) // delegate the task to the bowl
}

void resistAttack(Attack attack) {


this.door.resistAttack(attack) // delegate the task to the door
}
}

class Cage3 {
// create a cage for X-men

MeatBowl bowl = new MeatBowl();


AdamantiumDoor door = new AdamantiumDoor();
List<Animal> xmen = new ArrayList();

public Cage3 {
this.xmen.add(new Wolverine(...));
this.xmen.add(new Cyclops(...));
}
void feed() {
for(Animal t: this.xmen)
this.bowl.feed(t) // delegate the task to the bowl
}

void resistAttack(Attack attack) {


this.door.resistAttack(attack) // delegate the task to the door
}
}

class ZooGame {
void main() {
Cage1 tigerCage = new Cage1();
tigerCage.feed();
}
}

```

🐞 Lot of code repetition


I literally copy pasted!
Additionally, to add a new cage - for cats - I need to create a completely new class

The same issue from a dependency diagram perspective

```
------- --------- -------
IBowl Animal IDoor high level abstractions
------- --------- -------
║ ║ ║
║ ║ ║
┏━━━━━━━━━━┓ ┏━━━━━━━┓ ┏━━━━━━━━━━┓
┃ MeatBowl ┃ ┃ Tiger ┃ ┃ IronDoor ┃ low level implementation details
┗━━━━━━━━━━┛ ┗━━━━━━━┛ ┗━━━━━━━━━━┛
│ │ │
╰───────────────╁───────────────╯

┏━━━━━━━┓
┃ Cage1 ┃ high level code
┗━━━━━━━┛

```

In the above code, the high level code `Cage1` depends on the low level
implementation details `MeatBowl`, `Tiger`, `IronDoor`

=================================
⭐ Dependency Inversion Principle - what to achieve
=================================

- Code should NOT depend on low level implementation details


- instead, it should only depend on high level abstractions

- inversion of control: you don't call me, let me call you!

```
------- --------- -------
IBowl Animal IDoor high level abstractions
------- --------- -------
│ │ │
╰───────────────╁──────────────╯

┏━━━━━━┓
┃ Cage ┃
┗━━━━━━┛
```

But how?

=======================
💉 Dependency Injection - how to achieve it
=======================

- Don't create your dependencies yourself


- Instead, let your clients provide them

```java

interface IBowl { ... } // high-level abstraction


class FruitBowl implements IBowl { ... } // low-level implementation detail
class MeatBowl implements IBowl { ... } // low-level implementation detail
class GrainBowl implements IBowl { ... } // low-level implementation detail

interface IDoor { ... } // high-level abstraction


class WoodenDoor implements IDoor { ... } // low-level implementation detail
class IronDoor implements IDoor { ... } // low-level implementation detail
class AdamantiumDoor implements IDoor { ... } // low-level ...

class Cage { // high-level controller


// generic cage - will work for any usecase

IBowl bowl;
IDoor door;
List<Animal> occupants = new ArrayList();

// dependencies are getting injected here


// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
public Cage(IBowl bowl, IDoor door, List<Animal> occupants) {
this.bowl = bowl;
this.door = door;
this.occupants.addAll(occupants);
}

void feed() {
for(Animal t: this.occupants)
this.bowl.feed(t) // delegate the task to the bowl
}

void resistAttack(Attack attack) {


this.door.resistAttack(attack) // delegate the task to the door
}
}

class ZooGame {
void main() {
Cage tigerCage = new Cage(
new MeatBowl(...),
new IronDoor(...),
Arrays.asList(new Tiger(...), new Lion(...))
);
tigerCage.feed();

Cage birdCage = new Cage(


new FruitBowl(...),
new WoodenDoor(...),
Arrays.asList(new Sparrow(...), new Pigeon(...))
);
birdCage.feed();

}
}

```

If I do this, my code is smaller (got rid of 2 classes), yet, my client has more
power!

Dependency Inversion is achieved via Dependency Injection

Enterprise Code
===============

When you go to companies like Google - you will see extremely over engineered code!
- long names, various design patterns, all sort of weird things going around

If you go to Google, and you don't know LLD


very bad time! Everything looks so complex!

If you do know LLD


half of the time, you won't even have to the read the code
the filename/classname will tell you exactly what it does!

================
🎁 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


> Dependency Inversion Principle?
>
> A) A high-level module that depends on a low-level module
> through an interface
>
> B) A high-level module that depends on a low-level module directly
>
> C) A low-level module that depends on a high-level module
> through an interface
>
> D) A low-level module that depends on a high-level module directly
>

> ❓ What is the main goal of the Interface Segregation Principle?


>
> A) To ensure that a class only needs to implement methods that are
> actually required by its client
>
> B) To ensure that a class can be reused without any issues
>
> C) To ensure that a class can be extended without modifying its source code
>
> D) To ensure that a class can be tested without any issues

>
> ❓ 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
>

# ============================ That's all, folks! ===========================

You might also like