The core idea in object-oriented programming is to divide a program into smaller pieces and make each piece responsible for managing its own data.
This article provides a comprehensive but easily-to-understand approach to learning OOP. The objective is to understand the basic concepts of class-based object-oriented programming
At the end of the article, you will learn the following concepts:
Objects
Classes
Abstraction
Inheritance
Polymorphism
Encapsulation
Let's get started!
What is Object-Oriented Programming?
Object-oriented programming (OOP) is a programming methodology where all software units are organized around objects rather than functions and logic.
Like how cells form the basic unit of life, objects form the basic units of object-oriented programming.
OOP provides a way of creating and managing different aspects of your app in isolation from objects and connecting them independently. It also facilitates code reusability, provides cleaner maintainable code, and removes redundancy.
OOP consists of four pillars
Encapsulation
Abstraction
Inheritance
Polymorphism
These concepts will be detailed in our next sections, in the meantime, let's explore the importance of OOP
Importance of OOP
Object Oriented Program was introduced to remove the deficiency encountered in Procedural Programming.
The procedural style of programming uses a sequential approach and regards data and procedures (functions) as two different entities. Whenever you write your application using Procedural Programming, you will find that functions are scattered throughout the program as it develops.
The disadvantages of the Procedural approach are as below:
The Procedural code is often not reusable as you may have to copy and paste lines of code (recreate the code) if it is required in an aspect of the application.
Because priority is given to the procedures rather than the data, data is exposed to the entire application posing issues in some data-sensitive cases.
With the introduction of OOP, data and functionality are combined into a single entity called an object.
This makes it easier to create and manage different aspects of your app in isolation and connect them independently.
Because our application is built in units (objects) our code is well organized, more flexible, and easier to maintain ( avoiding "spaghetti code").
Understanding Objects in OOP
Objects are the core of Object-oriented programming. An object can represent a real-life entity with some data and behavior. It is a collection of related data (properties) and or functionality(methods).
Consider a common real-life object, a dog. The dog has certain characteristics: color, name, weight, etc. It can also perform some actions: bark, sleep, eat, etc.
In OOP terms, these characteristics of the dog are referred to as Properties
, and the actions are referred to as methods
.
In building a web app, each unit will be represented by an object.
Consider a scenario where you are developing a digital banking web app. An example of a real-world entity of the app will be a Person.
The Person
represents an object
consisting of properties
and methods
.
Examples of these properties could be first name, last name, date of birth, age, account details, and branch.
The Person
can have methods such as: logging in, changing the account details, checking the account balance, withdrawing and depositing funds, etc.
Below is a representation of the Person
object:
const Person = {
//properties
firstName: "Emmanuel",
lastName: "Kumah",
dob: "22/12/99",
branch: "Accra"
//methods
logIn(){
console.log(`Welcome ${this.firstName}`)
}
changeAccDetails(){
console.log('Account details changed')
}
checkAccBalance(){
console.log('Checking balance...')
}
withdrawFunds(){
console.log("Cash withdrawn")
}
depositFunds(){
console.log("Cash deposited")
}
}
Whenever Objects
are utilized, other parts of the app do not need to worry about what's going on inside an object. Because it provides an interface to other code that wants to use it, but it maintains its own private, internal state.
With our digital bank app, assuming we have over 25K persons on the app, we may have to declare 25K objects each with their unique properties
and methods
.
To solve this tedious task of creating 25K unique objects, JS introduces a concept called Classes
Classes and instances
Classes provide a way of grouping similar objects under one umbrella.
For instance, the over 25K persons on the app, can be categorized into users
Because each user represents an object and s*hares common properties and methods, we generally create a conceptual definition representing the **types of objects we can have in the application.*
Whenever we are dealing with a lot of objects in our app, we can avoid this repetition of writing properties and methods for each object and speed up development by defining a template or blueprint (which is a high-level plan) for creating these objects.
This template is referred to as a Class
What is a class?
Classes specify a layout to create objects with predefined properties and methods.
It can be likened to an Architect who has designed a blueprint for creating houses. Based on the blueprint, we can develop as many buildings as possible, each sharing common features defined in the blueprint.
The class lists the data and methods that will be included whenever a new object is created.
The basic Class
syntax is as below:
class MyClass {
constructor() { ... }
// class methods
method1() { ... }
method2() { ... }
method3() { ... }
...
}
Now, instead of defining each Person
object resulting in over 25K objects, we use a class
to design a layout to help us easily create these objects.
We will give our class the name User
since each Person
is a user of the app, and we will define all the properties
and methods
common to each user
Below is how the User
class is defined
class User{
// initialize properties
firstName,
lastName,
dob,
branch,
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//action performed by all users
logIn(){
console.log(`Welcome ${this.firstName}`)
}
}
The above defines a User
class with
Four
properites
: first name, last name, dob, branchThe
constructor
function: useful for creating the objectsThe
login()
methods available to all users.
Instances
A Class
does nothing on its own, it serves as a guide for creating specific objects. Each object we create becomes an instance of the class.
An instance is an object containing data and behavior defined by the class.
Constructor
In creating an instance we use a special method in the body of the class called a constructor.
Typically, the constructor is written out as part of the class definition. It enables assigning values to the properties we want to specify in the new instance.
Below is the snippet of the constructor in the User
class:
class User {
...
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
...
}
The constructor in the code above takes four parameters, so we can assign values to the firstName
, lastName
, dob
and branch
properties when we create a new User
object.
In the next section, we will learn how to create objects from a class.
Creating new Objects from a Class
We use the new
operator to create a new object (instance of the class) with all the listed methods and properties.
The syntax is as below:
new constructor(arg1, arg2,...)
Let's create an instance of the User
class in our earlier example using the new
operator.
//creating a new object from User class
const cus1 = new User (arg1, arg2,...)
When new User(arg1, ag2, ...)
is called:
A new object is created.
The
constructor
function (which has the same name as the Class) runs with the given arguments so it can assign the values to the new object
Below is how we create three different objects from the User
class in our app.
class User{
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the user
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
//create a user object from User class
const firstUser = new User('Emmanuel','Kumah','22/12/89','Accra')
const secondUser = new User('Robert','Taylor','12/12/77', "Accra")
const thirdUser = new User('Edmond',"Sims",'8/03/99', 'Accra')
//log the details
console.log(firstUser);
console.log(secondUser);
console.log(thirdUser)
The above creates three User objects in our app.
These three objects were derived from the defined
User
blueprint. Because we used theUser
blueprint, we did not need to write the entire code (both properties and methods) for each user we created.
The output of the code above is as below:
Each object can have access to the
logIn()
method declared in the class.See the code below
//access the methods defined in the User class
firstUser.logIn()
"Welcome Emmanuel"
In the next section, we explore Inheritance, one core pillar of OOP
Inheritance
Our digital banking app will definitely need clients who can log in and perform transactions ( deposit and withdraw cash).
We also need Managers who will be responsible for monitoring the activities of the bank.
The Client and BranchManager will have these properties and methods in common:
Properties: first name, last name, date of birth, and branch
Method: logIn()
Besides logging in to the app, the branch manager can view the details of all users and their transaction history. The client can also deposit and withdraw cash
Let's represent the BranchManager
and Client
as below:
class BranchManager {
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the branch manager
logIn(){
console.log(`Welcome ${this.firstName}`)
};
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
}
// Client class
class Client {
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the branch manager
logIn(){
console.log(`Welcome ${this.firstName}`)
};
depositCash(){
console.log('Cash deposited')
}
withdrawCash(){
console.log('Cash withdrawn')
}
}
We notice that the User class we defined earlier and the Branch Manager and Client classes above share certain properties and methods in common.
They both have the first name, last name, branch, and date of birth
They both have the
logIn()
method
Because they share certain properties and methods in common, it is generally recommended we define a class User
, that possesses all the common properties and methods for all other entities we will need in our app. This helps reduce redundancy
The class User
is defined as below:
class User {
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//common method to all entities
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
The class
User
defined above is referred to as the superclass or parent classWhenever we declare a parent class, we can declare child classes that will acquire all the properties and methods of the parent. This phenomenon is referred to as inheritance
Inheritance is the ability of a class to derive properties and characteristics from another class while having its own properties as well.
In real-life, inheritance is how children are able to acquire certain features like height, the color of hair, the shape of the nose, intelligence, etc, from their parents.
Similarly, in programming, the child classes can acquire all the properties and methods of the parent.
The goal of inheritance is to reuse common logic
Inherit using the extend and super keywords
The keyword extend
is used to show inheritance between classes and to declare the parent class we want to succeed from.
The syntax is as below:
childClassName extends parentClassName{
//child class definition
}
Using the keyword extends, we can instruct the BranchManager
and Client
classes to inherit all the properties and methods of the parent class User
. The BranchManager
and Client
classes can add additional properties and methods to their class.
In the code below, we extend the User
class to include the BranchManager
and Client
classes.
class User {
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
//Client class inherits from User
class Client extends User {
constructor(firstName, lastName, dob, branch, accType,amtDeposited){
//call the super before adding option
super(firstName, lastName, dob, branch);
//added new properties
this.accType = accType;
this.amtDeposited = amtDeposited
}
//additional methods
depositCash(){
console.log(`${this.firstName} deposited ${this.amtDeposited} USD`)
}
withdrawCash(){
console.log(`${this.firstName} withdrew cash`)
}
}
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
this.expLevl = expLevel;//added new props
}
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
}
//create two instances of the Client
const client1 = new Client('Ruby', 'Smit', '05/09/88', 'Accra', 'Savings', 20)
console.log(client1)
//access all methods of the Client class
client1.logIn()
client1.depositCash()
client1.withdrawCash()
// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)
console.log(firstManager)
//access the login method
firstManager.logIn()
//access the additional method
firstManager.viewUserDetails()
firstManager.viewTransactions()
The parent class is the
User
whiles the theBranchManager
andClient
are the child classes.Using the keyword
extend
, theBranchManager
andClient
inherits all the properties and methods of theUser
class.The
super
is used to call the constructor of its parent class to access the parent's properties and methods.The
this.expLevl = expLevel
,this.accType = accType
andthis.amtDeposited = amtDeposited
allows us to add extra properties to theBranchManager
andClient
classes respectively.We also added two extra methods
viewTransactions
andviewUserDetails
toBranchManager
In the
Cleint
class, we added thedepositCash
andwithdrawCash
methods.Because the
BranchManager
andClient
classes inherit from theUser
class, thelogIn()
has been added automatically, and can be accessed by instances of the class.
The concept of inheritance now allows any instance of the BranchManager
and Client
classes to inherit all properties and methods defined in the User
class.
Even though the logIn()
was not defined in the BranchManager
and Client
classes, we are still able to access it.
The output of the code above is as below:
Importance of inheritance:
Inheritance enables us to define a class that takes all the functionality from a parent class and allows you to add more
It helps in code reusability, provides cleaner maintainable code, and removes redundancy
Polymorphism
Polymorphism allows a method with the same name to have a different implementation in different classes.
In Polymorphism, a child class can overwrite a method that is inherited from a parent class. The goal is to permit greater flexibility and make code reusable.
For instance, the logIn()
method in the User
can be overwritten in the BranchManager
class.
See the code below:
//BranchManager class inherits from User
class BranchManager extends User {
//same code snippet used earlier
...
//overwrite method in superclass
logIn(){
console.log(`Howdy ${this.firstName}`)
}
}
const manager = new BranchManager('Simon','Tagoe','13/02/2000',"Accra",14)
//access the logIn() method
manager.logIn()
// output will be
Howdy Simon
Encapsulation
Encapsulation is the process of wrapping data and methods that work with them in a single component ( a Class). The concept of encapsulation is to allow only the object access to the state data. It is used to hide the values or state of the objects inside the class, to prevent modification by unauthorized code.
Its significance is to keep the data private to the object and avoid any direct access outside the object
Encapsulation encourages information hiding and reduces complexity by removing implementation details
Imagine using a TV remote control, you have some interface to work with such as the power button, and numbers button. You can use these interfaces for some actions, such as turning the TV on or off, changing the current TV channel, saving a favorite channel, etc.
You use these interfaces without bothering about how it works. In other words, the actual implementation of the interface is hidden from you.
Similarly, in OOP, you can use an object by callings its methods. Whether you wrote these objects yourself or utilized a third-party library, your code pays no attention to how the methods work internally.
The data contained in an object can only be accessed through a public interface (that is the object's own method). Whenever you want to use data contained in an object, you define a method within that object to handle the action.
It is considered bad practice to retrieve data inside an object and write separate code to perform actions with the data outside of the object.
For instance, if you want to promote a Branch Manager with more than 5years of experience to the next rank, we might have to implement it as below:
//create a instance of Branch Manager class
const branchManager = new BranchManager('firstName','lastName', '04/1/80', 7)
// code snippet to promote a manager
if(branchManager.expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
The code to promote a manager can be used at any required place in our system.
With this implementation, if we decide to change the criteria for promoting a manager, we will have to find everywhere the code is implemented in our app and update it.
However, the idea of encapsulation requires that data inside an object should only be accessed by the object's own method.
It would be better to have a promoteManager()
method within the BranchManager
class that handles the logic in just one place. So that when the logic is being updated, it is updated in only one place.
Below is the recommended procedure:
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
this.expLevl = expLevel;//added new props
}
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
//method to promote manager
promoteManager(){
if(this.expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
}
We can now access the promoteManager()
from the instance of the class
//access method from the instance of the class
branchManager.promoteManager()
Encapsulation: Private properties and method
An object's internal data can be kept private, making it accessible only by the object's own method and not from other objects.
For instance, if we don't make the expLevel
property private, someone can access it and change its value to this.expLevel = 10
To make the property private, we place an underscore ( _
) next to the property name. For instance (this._propertyName
)
Let's make the expLevl
private in the BranchManager
class
class User {
...
}
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
//protected property
this._expLevl = expLevel;
}
...
//method to promote manager
promoteManager(){
if(this._expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)
// try accessing the expLevl property and the output wil be undefined
console.log(firstManager.expLevl)
This implementation does not make the property truly private hence it's termed protected property.
To make the property truly private, we prepend the property with a #
and it should be defined outside any method. Eg. #firstName
We can also protect a method to prevent it from being accessed outside of the class.
Below is how to protect the promoteManager
method:
//protect the promoteManager method
_promoteManager(){
if(this._expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
//try accessing the method
manager.promoteManager()
The output of the code will be :
Uncaught TypeError: manager.promoteManager is not a function
The benefits of encapsulation are outlined below:
Data within an object, cannot be modified unexpectedly by an external code in an entirely different part of the application
Whenever we use a method, we only need to know the result and don't care about the internal implementation.
Functionality is defined in a logical place: that is the place the data is kept hence it becomes easy to alter the functionality of your application.
Abstraction
Abstraction is hiding or ignoring details that don't matter, allowing us to get an overview perspective of the thing we are implementing.
The objective of abstraction is to handle the complexity by hiding all irrelevant details from the user resulting in a simplified design that is easier to understand and maintain.
It is usually achieved by specifying methods for interacting with objects through interfaces. The implementation details of these methods are hidden behind these interfaces
This allows programmers to work with objects at a higher level of abstraction, without having to worry about the low-level details of their implementation.
For instance, whenever you purchase a smartphone, you may be only interested in the following
How to switch the phone on or off
How to dial a number to make a call
How to access information via the internet
The low-level details like
How does the phone gets connected to the internet to pull the specified information
What happens under the hood when you switch the phone on or off
How the gyroscope sensor works to enable you to rotate the phone
How the vibration, ambient temperature, magnetic field sensors, etc work are irrelevant to you hence these details can be hidden making the phone easy to use
Abstraction deals with simplification. For instance the Array
object in JavaScript enables storing a collection of multiple items under a single variable name.
It has methods like:
Array.length
that enables you to get the number of items in the arrayArray.push()
that pushes an element into the array etc
Whenever you use these methods, you don't need to know about all the intricacy that went into defining the Array
object. Those low-level details have been abstracted (removed) to make it easy to use the method. The goal is to simply use the methods expose to you to complete specific tasks.
One main benefit of abstraction is that it reduces the impact of change. The developers of the Array
object can change the internal implementation, for instance, to improve its performance. However, as long as there is still array.push()
and array.length
methods, you can continue to use the Array
methods just like before without being negatively impacted by the changes
Summary
The core idea in object-oriented programming is to divide a program into smaller pieces and make each piece responsible for managing its own state.
Classes specify a layout to create objects with predefined properties and methods.
Inheritance is the ability of a class to derive properties and characteristics from another class while having its own properties as well.
Polymorphism allows a method with the same name to have a different implementation in different classes.
Encapsulation is the process of wrapping data and methods that work with them in a single component ( a Class).
Abstraction is hiding or ignoring details that don't matter, allowing us to get an overview perspective of the thing we are implementing.
If you have found value in this article, or seen areas of improvement, please do comment. Also kindly share the article with your social networks, it might be beneficial to someone else.
Top comments (6)
If you're learning OOP in JS - it's a good idea to learn about prototypal inheritance which is the model that JS uses. Sure, it has recently acquired
class
keywords etc. but these are just syntactic sugar over the prototype system - to make it look like JS uses class-based OOP.Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍
Very informational article. Thanks for sharing :)
Very good article. Thanks!
What an interesting article. It reminds me when I was learning PHP at varsity.
These concepts are useful someone ready to dive into the parts of system design