Hello and Welcome Back, if you are reading this one without reading OOP Part 1, I will suggest you take a look first at Part 1 of this OOP for a clear understanding of what is Classes and how it is useful in creating big project,
Previously, on part 1 we have explored and got the deeper understanding of how the OOP works, what are the different method available to create an object and how Classes act as a blueprint to create a new Object making code reusable.
In this Part we gonna deep dwell into the topic called as the 4 pillars of the Object Oriented Programming.
The Four Principles of OOP :
Object Oriented Programming, is usually governed by four key principles namely Inheritance, encapsulation, abstraction and polymorphism respectively.
Inheritance:
Inheritance in OOP represents how we can create a class with the pre-existing class called "Parent class" and use that class properties and methods in this new class called "Child class" by adding some new properties and methods.
Inheritance allows us to inherit the properties and methods present inside the parent class and use them effectively to create our new child class with additional properties or methods.
Let's see this with an example. Imagine all the characters we defined before in Part 1 can also have the power "property" and attack "method". One way to implement that would be just to add the same properties and methods to all the classes we had, like this:
class Marvel { // Name of the class
// The constructor method will take a number of parameters and assign those parameters as properties to the created object.
constructor (name, phrase, power) {
this.name = name
this.phrase = phrase
this.power = power
this.member = "Avengers"
}
// These will be the object's methods.
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've an got attacking power of ${this.power}!`)
}
class DC {
constructor (name, phrase, power) {
this.name = name
this.phrase = phrase
this.power = power
this.member = "Justice League"
}
hide = () => console.log("Catch us if you can!")
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class XMEN {
constructor (name, phrase, power) {
this.name = name
this.phrase = phrase
this.power = power
this.species = "X-men"
}
isMutant = () => console.log("yes")
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
As from we can see how we have added the power property to the all the classes, But you can see we're repeating code, and that's not optimal. A better way would be to declare a parent "Power" class which is then extended by all Universe Class, like this:
class Power {
constructor(power) {
this.power = power
}
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power { // Name of the class
// The constructor method will take a number of parameters and assign those parameters as properties to the created object.
constructor (name, phrase, power) {
super(power)
this.name = name
this.phrase = phrase
this.member = "Avengers"
}
// These will be the object's methods.
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
sayPhrase = () => console.log(this.phrase)
}
//Similarly we can extend all the Class of DC and XMEN
See that the power class looks just like another class, and we used a similar concept of properties and Methods in the power class.
On the children class, we use the extends
keyword to declare the parent class we want to inherit from. Then on the constructor method, we have to declare the "power" parameter and use the super
function to indicate that the property is declared on the parent class.
When we instantiate new objects, we just pass the parameters as they were declared in the corresponding constructor function and bazinga! We can now access the properties and methods declared in the parent class.
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10)
const marvel_2 = new Marvel("Thor", "I'm still worthy!", 30)
marvel_1.attack() // output: I've got an attacking power of 10!
console.log(marvel_2.power) // output: 30
Now, let's suppose we want to create the Universe class such that it Groups all the classes no, matter what universe they belong to and we want to set a property of "speed" and a "move" method. We can do that like this:
class Universe {
constructor (speed) {
this.speed = speed
}
move = () => console.log(`I'm moving at the speed of ${this.speed}!`)
}
class Power extends Universe {
constructor(power,speed) {
super(speed)
this.power = power
}
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power{
constructor (name, phrase, power,speed) {
super(power,speed)
this.name = name
this.phrase = phrase
this.member = "Avengers"
}
// These will be the object's methods.
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
sayPhrase = () => console.log(this.phrase)
}
//Similarly we can extends other class DC and XMEN
First we declare the new "Universe" parent class. Then we extend it on the "Power" class. And finally, we add the new "speed" parameter to the constructor
and super
functions in our Marvel class.
We instantiate passing the parameters as always and bazinga! again, we can access properties and methods from the "grandparent" class.
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
const marvel_2 = new Marvel("Thor", "I'm still worthy!", 30, 60)
marvel_1.move() // output: "I'm moving at the speed of 50!"
console.log(marvel_2.speed) // output: 60
Now, as of now, we know more about Inheritance we can now move forward how we can relatively avoid the code repetition as low as possible.
class Universe {
constructor (speed) {
this.speed = speed
}
move = () => console.log(`I'm moving at the speed of ${this.speed}!`)
}
class Power extends Universe {
constructor(name, phrase, power, speed) {
super(speed)
this.name = name
this.phrase = phrase
this.power = power
}
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Avengers"
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
}
class DC extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Justice League"
}
hide = () => console.log("Catch us if you can!")
}
class XMEN extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "X-men"
}
isMutant = () => console.log("yes")
}
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
// We use the "new" keyword followed by the corresponding class name
// and pass it the corresponding parameters according to what was declared in the class constructor function
const marvel_2 = new Marvel("Thor", "I'm still worthy!", 30, 60)
const dc_1 = new DC("Batman", "I am Batman!",25, 100)
const dc_2 = new DC("Aquaman", "Tide is Calling!", 5, 120)
const xmen_1 = new XMEN("Wolverine", "So... This Is What It Feels Like!", 125, 30)
const xmen_2 = new XMEN("Pheonix", "I am Jean Grey!", 155, 40)
See that our "member" classes look much smaller now, thanks to the fact that we moved all shared properties and methods to a common parent class. That's the kind of efficiency inheritance can help us with.
Some Things to keep in Mind while working with Inheritance:
A class can only have one parent class to inherit from. You can't extend multiple classes, though there are hacks and ways around this.
You can extend the inheritance chain as much as you want, setting parent, grandparent, great-grandparent classes and so on.
If a child class inherits any properties from a parent class, it must first assign the parent properties calling the
super()
function before assigning its own properties., For E.g.//This will work Fine class Marvel extends Power { constructor(name, phrase, power, speed){ super(name, phrase, power, speed) this.member = "Avengers" } canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!") } //But this gives an reference error class Marvel extends Power { constructor(name, phrase, power, speed){ this.member = "Avengers" super(name, phrase, power, speed)//ReferenceError: Must call super constructor in derived class //before accessing 'this' or //returning from derived constructor } canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!") }
When inheriting, all parent methods and properties will be inherited by the children. We can't decide what to inherit from a parent class (same as we can't choose what virtues and defects we inherit from our parents. We'll get back to this when we talk about composition).
Children's classes can override the parent's properties and methods.
to give an example what this point constitutes, we know that our Marvel class inherit the method "attack" from the class "Power, when then can log
I'm attacking with a power of ${this.power}!
.
class Power extends Universe {
constructor(name, phrase, power, speed) {
super(speed)
this.name = name
this.phrase = phrase
this.power = power
}
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Avengers"
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
}
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
marvel_1.attack() // output: I've got an attacking power of 10!
Let's say we want the attack
method to do a different thing in our Marvel class. We can override it by declaring it again, like this:
class Power extends Universe {
constructor(name, phrase, power, speed) {
super(speed)
this.name = name
this.phrase = phrase
this.power = power
}
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Avengers"
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
attack= () => console.log("Now I'm doing a different thing, HA!")
}
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
marvel_1.attack() // output: "Now I'm doing a different thing, HA!"
Encapsulation:
Encapsulation is another key concept in OOP, and it stands for an object's capacity to "decide" which information it exposes to "the outside" and which it doesn't. Encapsulation is implemented through public and private properties and methods.
In JavaScript usually all the methods and properties are public by default, which means we can access the methods and properties of an object outside its own body for e.g:
class Marvel extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Avengers"
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
}
//// Here's our object
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
// Here we're accessing our public properties and methods
console.log(marvel_1.name) // output: Iron Man
marvel_1.sayPhrase() // output: "I'm Iron Man!"
To make this clearer, let's see how private properties and methods look like.
Let's say we want our Marvel class to have a actor
property, and use that property to execute a whoPlayed
the role method, but we don't want that property to be accessible from anywhere else other than the object itself. We could implement that like this:
class Marvel extends Power {
#actor //We first need to declare the private property, always using the '#' symbol as the start of its name.
constructor(name, phrase, power, speed, actor){
super(name, phrase, power, speed)
this.member = "Avengers"
this.#actor = actor //Then we assign its value within the constructor function
}
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")//
whoPlayed = () => console.log(`The role Played by ${this.#actor}`)//// and use it in the corresponding method.
}
// We instantiate the same way we always do
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50, "Robert Downey Jr.")
Then we can access the howOld
method, like this:
marvel_1.whoPlayed() ///output : The role Played by Robert Downey Jr.
But if we try to access the property directly, we'll get an error. And the private property won't show up if we log the object.
console.log(marvel_1.#actor) // This throws an error
console.log(marvel_1)
// output:
// Marvel {
// move: [Function: move],
// speed: 50,
// sayPhrase: [Function: sayPhrase],
// attack: [Function: attack],
// name: 'Iron Man',
// phrase: "I'm Iron Man!",
// power: 10,
// canfly: [Function: fly],
// whoPlayed: [Function: whoPlayed],
// member: 'Avengers'
// }
Encapsulation is useful in cases where we need certain properties or methods for the inner working of the object, but we don't want to expose that to the exterior. Having private properties/methods ensures we don't "accidentally" expose information we don't want.
Abstraction :
Abstraction is a principle that says that a class should only represent information that is relevant to the problem's context. In plain English, only expose to the outside the properties and methods that you're going to use. If it's not needed, don't expose it.
This principle is closely related to encapsulation, as we can use public and private properties/methods to decide what gets exposed and what doesn't.
Polymorphism :
Then there is polymorphism (sounds really sophisticated, doesn't it? OOP names are the coolest). Polymorphism means "many forms" and is actually a simple concept. It's the ability of one method to return different values according to certain conditions.
For example, we saw that the Power class has the sayPhrase
method. And all our species classes inherit from the Enemy class, which means they all have the sayPhrase
method as well.
But we can see that when we call the method on different species, we get different results:
const marvel_2 = new Marvel("Thor", "I'm still worthy!", 30, 60)
const dc_1 = new DC("Batman", "I am Batman!",25, 100)
marvel_2.sayPhrase() //output: "I'm still worthy!"
dc_1.sayPhrase() //"I am Batman!"
And that's because we passed each class a different parameter at instantiation. That's one kind of polymorphism, parameter-based.
Another kind of polymorphism is inheritance-based, and that refers to when we have a parent class that sets a method and the child overrides that method to modify it in some way. The example we saw previously on the Inheritance where we change the "attack" method during child creation of Instance applies perfectly here as well:
class Power extends Universe {
constructor(name, phrase, power, speed) {
super(speed)
this.name = name
this.phrase = phrase
this.power = power
}
sayPhrase = () => console.log(this.phrase)
attack = () => console.log(`I've got an attacking power of ${this.power}!`)
}
class Marvel extends Power {
constructor(name, phrase, power, speed){
super(name, phrase, power, speed)
this.member = "Avengers"
}
canfly = () => console.log("ssssssssssssshhhhhhhhhhh!!")
attack= () => console.log("Now I'm doing a different thing, HA!")
}
const marvel_1 = new Marvel("Iron Man", "I'm Iron Man!", 10, 50)
marvel_1.attack() // output: "Now I'm doing a different thing, HA!"
This implementation is polymorphic because if we commented out the attack
method in the Marvel class, we would still be able to call it on the object.
marvel_1.attack() //output : "I've got an attacking power of 10!"
We got the same method that can do one thing or another depending if it was overridden or not called Polymorphic. In this way, we call the Ploymorphism properties a type of Inheritance that can be Parameter-based where we can pass each member with a different "sayPhrase()" method OR Polymorphism can be Inheritance-based, which refers to when we have a parent class that sets a method and the child overrides that method to modify it in some way.
Conclusion :
OOP is a very powerful programming paradigm that can help us tackle huge projects by creating the abstraction of entities. Each entity will be responsible for certain information and actions, and entities will be able to interact with each other too, much like how the real world works.
In this article we learned about classes, inheritance, encapsulation, abstraction, polymorphism. These are all key concepts in the OOP world. And we've also seen various examples of how OOP can be implemented in JavaScript.
In this Next Article, I will be more please to say, that I am gonna Explore the the one More COncept of OOP in Javascript called "Object Composition".
please leave a comment if you have any questions or feedback.
I've learned this stuff on the internet as well, Here are the resources given and tried to simplify it from the user's perspective.