Introduction
Object-Oriented Programming (OOP) is a popular approach to computer programming. It is a way of organizing and structuring code to model real-world entities (things) or “objects” and their interactions.
It makes programming more intuitive because humans think and interact with the world in terms of objects and what is known about them and what they can do.
What is Object-Oriented Programming (OOP)?
To best understand OOP, it is worth considering why it emerged or what it solves. You have worked with objects created through plain object syntax. Perhaps you wondered why these types of constructs came about, why not just use variables.
In the earlier days of programming, code bases were simply a collection of variables and functions, typically declared at the highest scope. This made it rather cumbersome to arrange, navigate and think about systems and code functionality.
Consider the example below which is part of a coffee order system. Each coffee order has information to keep track of; an id, type, quantity and price and would be stored in a variable in global scope.
// Global scope variables for three coffee orders
// First order
let order1Id = 1;
let order1Type = "Latte";
let order1Quantity = 2;
let order1Price = 5.0;
// Second order
let order2Id = 2;
let order2Type = "Espresso";
let order2Quantity = 1;
let order2Price = 3.0;
// Third order
let order3Id = 3;
let order3Type = "Cappuccino";
let order3Quantity = 3;
let order3Price = 4.5;
function displayOrders(orderId, orderType, orderQuantity, orderPrice) {
console.log(
`Order Info: ID=${orderId}, Type=${orderType}, Quantity=${orderQuantity}, Price=$${orderPrice}`
);
}
// Display the orders
displayOrders(order1Id, order1Type, order1Quantity, order1Price);
This means that your script would have a whole bunch of variables mixed up together. This would make it rather difficult to know which variables belong to which order and how to interact and manage the data let alone manage their scope and protect the values from being changed incorrectly. Each variables would need to be manually organized and named appropriately so that you know which variables belong to a given order. You can see how this approach is not particularly desirable. Especially when working with a lot more coffee orders.
The Object-Oriented Programming approach emerged within the programming community to solve this problem. Instead data for a given entity or thing is grouped into objects. This way all the data belonging to that object is in one place. This makes it easier and more intuitive to code and design systems. This is illustrated below using plain old Javascript objects (POJO), which you have used before.
// Coffee order objects
let order1 = {
id: 1,
type: "Latte",
quantity: 2,
price: 5.0,
};
let order2 = {
id: 2,
type: "Espresso",
quantity: 1,
price: 3.0,
};
let order3 = {
id: 3,
type: "Cappuccino",
quantity: 3,
price: 4.5,
};
function displayOrders(order) {
console.log(
`Order: ID=${order.id}, Type=${order.type}, Quantity=${order.quantity}, Price=$${order.price}`
);
}
// Display the orders
displayOrder(order1);
With the above refactored version, all the data for the orders id, type, quantity and price is stored in an object. So instead of needing to pass the four loose variables into the function, we can just pass in one object. Another example would be when you want to update some data. Lets say the quantity for order1 and order3 needs to be increased by two. The previous approach would require the developer to look through all 13 variables to make sure the correct data is updated. But with the OOP approach, each objects quantity can be easily referenced and updated. Below shows these two different scenarios.
// loose variables
order1Quantity = 3;
order3Quantity = 4;
// OOP
order1.quantity = 3;
order3.quantity = 4;
There are many more examples of how OOP makes coding more intuitive compared to its predecessor approaches. You will experience these the more you work with OOP.
OOP is a vast subject and has numerous conventions and principles. These conventions and principles are crucial for OOP-based languages typically used in backend web development like C# and Java because they help in managing complex, large-scale systems by promoting code reuse, scalability, and maintainability. Backend systems often require handling numerous interactions, data manipulations, and business logic, which can be efficiently managed using OOP principles such as encapsulation, inheritance, and polymorphism.
On the other hand, frontend development typically focuses more on user interfaces and immediate user interactions. While OOP principles can still be beneficial, frontend development often deals with smaller, more modular components and may prioritize flexibility and speed of development over the strict structure and organization provided by OOP conventions. Thus, the need for rigorous adherence to OOP principles is less pronounced in the frontend compared to the backend.
There are some core concepts and principles that you need to understand about OOP, namely encapsulation, information hiding, inheritance and polymorphism. In order to understand these, we need to understand objects are defined and created in modern JS development, using classes.
Classes
A class is a blueprint or template for objects. It defines the fields (variables) and methods (functions) that objects of that class will have. Classes are like cookie cutters, and objects are like the cookies you make from those cutters. They will all have the same structure.
Why is this useful? Consider the example below:
const myFirstOrderPojo = {
orderId: 1,
total: 190,
date: "10-30-2023",
};
const mySecondOrderPojo = {
orderId: 2,
totals: 70,
date: "11-30-2023",
};
Notice that the total
property is different in the second object (totals
), it should be total, these two objects don’t have the same structure. Creating objects in this way makes it possible to accidentally define inconsistent object structures.
Classes provide a consistent way of creating objects as opposed to needing to repeatedly create plain old JS objects (POJO) and manually ensure the properties are consistent.
Anatomy of a Class
Classes are typcially made up of four main parts, fields (variables), methods (functions), constructors and properties (getters and setters). The class below demonstrates fields, methods and a constructor. Getters and setters are shown when we discuss information hiding.
Consider the following class which contains a constructor, a method called myMethod
and three fields myInitial
, basicProperty
and emptyProperty
:
class MyClass {
// This is a field in our class
basicField = "My basic field";
// This is another field however it isn't initialized to a value
emptyField;
// The constructor lets us set values when we create an instance of a class
myInitial;
constructor(myInitialValue, emptyFieldValue) {
// A field 'myInitial' being initialized to an argument
this.myInitial = myInitialValue;
// A field 'emptyProperty' being initialized to an argument
this.emptyField = emptyFieldValue;
}
// A method in the class
myMethod() {
console.log(this.myInitial);
}
}
As mentioned, a class is just a template of an object. We can create an object with the class, called an “instance”, which then has all of the properties and methods from the original class.
Creating Objects
We create a new instance of a class by using the new
keyword.
const myClassInstance = new MyClass("Hello world", 3);
We can now call the myMethod
method from this class or for example log out the value of the emptyField:
// Logs "Hello world"
myClassInstance.myMethod();
console.log(myClassInstance.emptyField);
Let’s look at a more relevant example.
class Order {
// fields
orderId;
total;
date;
// constructor
constructor(ordId, total, date) {
this.orderId = ordId;
this.total = total;
this.date = date;
}
// method/s
printReceipt() {
console.log(
`Receipt Id: ${this.orderId}, Date: ${this.total} Total: ${this.date}`
);
}
}
The above Order
class has three fields (orderId, total and date), one method (printReceipt) and a constructor.
Fields
The fields are the variables that store the data for an object. It represents what is known about an object, its total or date etc.
Constructor
You can create multiple objects from a single class, each with its own set of data and behaviours.
We use the new keyword and then the name of the Class with parenthesis like a function. This will call the constructor to construct the object. Typically, the constructor (like in the Order
class) has parameters so that we can provide values for the objects fields in line, instead of setting values for each field individually.
const myFirstOrder = new Order(1, 190, "10-30-2023");
const mySecondOrder = new Order(2, 70, "11-30-2023");
The constructor in a class allows us to set any initial field values or allow for values to be passed in as arguments as shown in the Order
class above.
NOTE: You do not need to make use of a constructor if you have nothing to initialize.
Methods
Methods are functions inside a class. They are copied when we create a new instance of a class.
We don’t use the function
keyword when creating a method in a class, we simply write the method name:
printReceipt() {
console.log(
`Receipt Id: ${this.orderId}, Date: ${this.total} Total: ${this.date}`
);
}
NOTE
The two objects created above with the Order class (myFirstOrder
and mySecondOrder
) are guaranteed to have the same structure (fields and methods) when created using the Order
class. The fields and methods are defined once in the class, no need to repeat for each object.
NOTE
Practical example 1: Person object
Now that we’ve covered the basics, let’s start looking at a more practical example:
The following is a basic example of a class called User
. It takes in firstName
and lastName
parameters and has a method greetUser()
which logs a greeting message.
class User {
language = "Norwegian";
constructor(firstName, lastName) {
// Set 'firstName' to the 'firstName' parameter
this.firstName = firstName;
// Set 'lastName' to the 'lastName' parameter
this.lastName = lastName;
}
// 'greetUser' method that logs a greeting message
greetUser() {
console.log(
`Hello ${this.firstName} ${this.lastName}! Language: ${this.language}`
);
}
}
const newUser = new User("Ola", "Nordmann");
// Logs "Hello Ola Nordmann! Language: Norwegian"
newUser.greetUser();
Practical example 2: Shopping Cart
We are going to have a look at a more complex implementation of a class.
In this example, we are going to create a Shopping Cart.
This class has:
addToCart
: A function to add to the cart.removeFromCart
: Removes an item from the cart.calculateTotalCost
: Calculates the total cost of the items in the cart.displayCart
: Displays the items from the cart.displayTotalCost
: Displays the total cost of the items in the cart.
class ShoppingCart {
cart = [];
constructor(shopName, currency) {
this.shopName = shopName;
this.currency = currency;
}
/** Adds the item to the cart **/
addToCart(item) {
this.cart.push(item);
}
/** Removes the item from the cart **/
removeFromCart(item) {
const idToFind = item.id;
// Find the index to remove
const indexToRemove = this.cart.findIndex(
(currentItem) => currentItem.id === idToFind
);
// If the index is -1 then it means no item was found, so
// we return null to break out of the function
if (indexToRemove === -1) {
return null;
}
// Filter the items and remove the item that matches our index
const newCart = this.cart.filter((item, index) => index !== indexToRemove);
// Set the cart to cart without the item by spreading out the array
this.cart = [...newCart];
}
/** Calculates the total cost of items in the cart **/
calculateTotalCost() {
const totalCost = this.cart.reduce((total, item) => {
total += item.price;
return total;
}, 0);
return totalCost;
}
/** Displays the items in the cart **/
displayCart() {
console.log("Your cart:");
console.log("-------------------");
this.cart.forEach((item) => {
console.log(item.title);
});
console.log("===================");
}
/** Displays the total cost of the items in the cart **/
displayTotalCost() {
console.log("Total items: ", this.cart.length);
console.log("The total of the cart is:", this.calculateTotalCost());
}
}
const myCart = new ShoppingCart("Norway Bakery", "USD");
const cookies = { id: 23, title: "Chocolate Chip Cookies", price: 20.0 };
const cake = { id: 45, title: "Vanilla Cake", price: 30.0 };
myCart.addToCart(cookies); // Add an item
myCart.addToCart(cookies); // Add an item
myCart.addToCart(cake); // Add an item
myCart.displayCart(); // Display the cart
myCart.removeFromCart(cookies); // Remove an item
myCart.displayCart(); // Display the cart
myCart.displayTotalCost(); // Display total cost of the cart
Lesson task
Goal
To be able to create a class, add methods and then create an instance of that class.
Brief
We are going to create a basic class based on what you have learnt so far.
NOTE: Lesson tasks do not get submitted on Moodle and are not assessed by tutors. They are mainly there for you to practise what you have learned in the lesson.
Level 1 process
Create a class called
Person
.It should take in
firstName
andlastName
arguments.Add a method
introduce
that logs"Hello I am Ola Nordmann"
if I was to supplyOla
as thefirstName
andNordmann
as the last name.Create a new instance of this class and then call the
introduce
method from this instance.