Important Concepts in Object-Oriented Programming
20 January, 2023
4
4
1
Contributors
Important concepts in OOP
The four pillars of object-oriented programming (OOP) are encapsulation, inheritance, polymorphism and abstraction.
Encapsulation
Encapsulation: Encapsulation is the practice of hiding the internal details of an object and only exposing a public interface. This allows data and behaviour to be bundled together and protected from external modification, which promotes data integrity and security. For example, in Java, a class can have private fields and methods only accessed by the class's public methods.
In JavaScript, encapsulation can be achieved by using closures
and the this
keyword.
A closure is a function that has access to variables in its parent scope, even after the parent function has finished executing. This allows data to be hidden within the closure and only exposed through a public interface.
Here's an example of encapsulation in JavaScript using closures:
function createPerson() {
let name = "John Doe";
let age = 30;
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
},
getAge: function() {
return age;
},
setAge: function(newAge) {
age = newAge;
}
};
}
let person = createPerson();
console.log(person.name); //undefined
console.log(person.getName()); // "John Doe"
person.setName("Jane Smith");
console.log(person.getName()); // "Jane Smith"
In this example, the createPerson
function creates an object with a name
and an age
property hidden within the closure. The object also has getName
, setName
, getAge
, and setAge
methods, which are the only way to access and modify the name
and age
properties.
Another way to encapsulate in javascript is by using the this
keyword, where the this
keyword refers to the object that the method is being called on.
let person = {
name: "John Doe",
age: 30,
getName: function() {
return this.name;
},
setName: function(newName) {
this.name = newName;
},
getAge: function() {
return this.age;
},
setAge: function(newAge) {
this.age = newAge;
}
};
console.log(person.name); // "John Doe"
console.log(person.age); // 30
console.log(person.getName()); // "John Doe"
person.setName("Jane Smith");
console.log(person.getName()); // "Jane Smith"
In this example, the name
and age
properties are directly accessible. Still, by using the this
keyword inside the object's methods, it's making it clear that these properties should be accessed and modified only through these methods.
It's worth noting that these examples need to provide full encapsulation like in other languages. Still, it's providing a way to restrict direct access to the properties and make it clear that these properties should be accessed and modified only through these methods.
Inheritance
Inheritance: Inheritance is the ability of one class to inherit properties and methods from a parent class. This allows for code reuse and a more organized class hierarchy. For example, a class "Bird" can inherit properties and methods from a class "Animal" and add its unique properties and methods. In JavaScript, inheritance can be achieved through prototypes and the Object.create()
method.
The Object.create()
method creates a new object with a specified prototype. You can use this method to create an object that inherits from another object, like this:
let parent = {
name: "John Doe",
age: 30,
getName: function() {
return this.name;
}
};
let child = Object.create(parent);
child.name = "Jane Smith";
child.age = 5;
console.log(child.getName()); // "Jane Smith"
console.log(child.age); // 5
console.log(child.__proto__ === parent); // true
In this example, the child
object is created using Object.create(parent)
, which sets the child
object's prototype to parent
. The child
object has its own properties name
and age
, but it also inherits the getName
method from the parent
object, and can access its name
property via the prototype chain.
Another way to achieve inheritance in javascript is by using the Object.setPrototypeOf()
method, which allows you to set an object's prototype to another object.
let parent = {
name: "John Doe",
age: 30,
getName: function() {
return this.name;
}
};
let child = {
age: 5
};
Object.setPrototypeOf(child, parent);
console.log(child.getName()); // "John Doe"
console.log(child.age); // 5
console.log(child.__proto__ === parent); // true
In this example, we have created an object child
with its own property age
, and then set its prototype to parent
object using Object.setPrototypeOf(child,
parent)
Now the child
object has access to the properties and methods of the parent
object through the prototype chain.
It's worth noting that this way of inheritance can be less performant than the Object.create()
method, and it's not recommended to use __proto__
property as it's not a standard way.
It's also worth noting that javascript has a class syntax (class
keyword) for creating objects which is just a syntax sugar on top of the prototype-based inheritance.
Polymorphism
Polymorphism: Polymorphism is the ability of different objects to respond to the same method call differently. This allows for objects of different types to be treated as a single type and for code to be written in a more general and flexible way. For example, a method that accepts an object of class "Animal" can accept an object of class "Bird" as well because the "Bird" class is inherited from the "Animals" class.
In JavaScript, polymorphism can be achieved through the use of function overloading and function overriding.
Function overloading is the ability of a function to handle multiple types of arguments, and function overriding is the ability of a child object to override the methods of its parent object.
Function overloading can be achieved using the arguments
object, an array-like object that contains all the arguments passed to a function. You can check the type of each argument and perform different actions based on the type.
function add(a, b) {
if (typeof a === 'string' && typeof b === 'string') {
return a + ' ' + b;
} else if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
}
console.log(add(1,2)); // 3
console.log(add("Hello","world")); // "Hello world"
In this example, the add
function can handle numbers and strings as arguments and perform different actions based on the type.
Function overriding can be achieved by creating an object that inherits from another object and then giving it its implementation of a method.
let parent = {
name: "John Doe",
getName: function() {
return this.name;
}
};
let child = Object.create(parent);
child.getName = function() {
return "I'm " + this.name;
};
console.log(child.getName()); // "I'm John Doe"
In this example, the child
object inherits the name
property and the getName
method from the parent
object but overrides the getName
method with its implementation.
It's worth noting that JavaScript is a dynamic language, and the type of an object can be changed at runtime, so it's easy to achieve polymorphism by changing the type of an object or adding/removing properties and methods.
It's also worth noting that JavaScript does not have strict type checking, so you need to be extra careful when using polymorphism by checking the type of the arguments and using the instanceof
operator to check the type of an object.
Abstraction
Abstraction: Abstraction is the ability to focus on the essential features of an object and ignore the non-essential details. This allows for the creation of abstract classes and interfaces that define a group of objects' common properties and methods without specifying their exact implementation. For example, a class "Vehicle" can be an abstract class, it can have common properties and methods for all types of vehicles, but the specific implementation of those methods will be different for different types of vehicles.
In JavaScript, abstraction can be achieved through abstract classes, interfaces, and higher-order functions.
An abstract class is a class that cannot be instantiated but can be extended by other classes. An interface is similar to an abstract class, but it only contains method signatures and cannot contain any implementation.
JavaScript does not have built-in support for abstract classes and interfaces. Still, you can achieve similar functionality by combining techniques such as closures, the Object.create()
method, and the this
keyword.
Here's an example of an abstract class in JavaScript:
function AbstractShape() {
if (new.target === AbstractShape) {
throw new TypeError("Cannot construct Abstract instances directly");
}
this.x = 0;
this.y = 0;
}
AbstractShape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
};
function Rectangle() {
AbstractShape.call(this);
}
Rectangle.prototype = Object.create(AbstractShape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.draw = function() {
console.log("Drawing a rectangle at: (" + this.x + ", " + this.y + ")");
};
let rect = new Rectangle();
rect.move(10,10);
rect.draw(); //Drawing a rectangle at: (10, 10)
In this example, the AbstractShape
class is created using a closure, it has x
and y
properties and a move
method that are common to all shapes. The Rectangle
class extends the AbstractShape
class and has its draw
method specific to rectangles. The AbstractShape
class is not supposed to be instantiated, so it throws an error if anyone tries to instantiate it directly.
Interfaces can be achieved in javascript by creating an object with method signatures and then checking that the objects that need to implement the interface have all the methods defined in the interface.
Higher-order functions are functions that take other functions as arguments or return functions as results, it can be used for abstraction, for example:
function add(a) {
return function(b) {
return a + b;
}
}
let add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(4)); // 9
Here, add
is a higher-order function that takes a number as an argument and returns a new function that takes another number and returns the sum of the two numbers. This way, you can create abstraction in JavaScript.
It's worth noting that these concepts work together to create complex, modular, and reusable code and help developers design and organize their code efficiently.