Encapsulation
Recall, encapsulation is the concept of bundling data (fields/variables) and the methods (functions) that operate on that data into a single unit (an object). We have seen why this is useful. The next consideration is how to control the data inside of object. Consider the example below using the previous Order
objects
order1.orderId = 3
order3.orderId = 9
Notably, any field or method defined in a class or function constructor is publicly available by default. This will allow any statement to alter any property directly which could lead to unexpected behaviour. Hence, a core principle of OOP is that fields should be made private, and then methods are defined to provide access to them.
The problem here is that there is no control over the changing of these values, in this case the id field. This is not ideal as they could be incorrectly update or by someone or some code who is not authorized to do so. There is nothing protecting these values. In OOP the rule that the data hiding principle is that fields should be private (hidden). Because this is a common practice the term encapsulation and data/information hiding are often used interchangeably.
Hiding fields
JavaScript supports private variables using the # symbol in classes. Making a variable private will ensure that it cannot be directly accessed and changed from outside the class.
class Order {
#orderId; // hidden (private)
constructor(ordId, total, date) {
this.#orderId = ordId;
this.total = total;
this.date = date;
}
printReceipt() {
console.log(
`Receipt Id: ${this.#orderId}, Date: ${this.date} Total: ${this.total}`
);
}
}
const myOrder = new Order(1, 70, "11-30-2023");
console.log(myOrder.orderId); // output: undefined
myOrder.orderId = 2; // no effect
console.log(myOrder.total); // output: 70
myOrder.printReceipt(); // output: Receipt Id: 1, Date: 70 Total: 11-30-2023
Now the orderId
is not directly accessible on the object, it simply shows undefined
, and has no effect when attempting to change the value. Being private it cannot be directly accessed and changed. However, internal methods such as printReceipt
still have access to it. This is what is meant by data hiding. orderId
is hidden from direct access but used internally.
Methods are typically left to be public as there is no risk of changing a variable value, as methods are simply invoked. Methods can however be made private. In the same way that JavaScript classes support private fields, it can also create private methods. These methods are only available within the class and can not be used on the instance of a class. Private methods are also created using the #
symbol.
class Order {
#orderId; // hidden (private)
constructor(ordId, total, date) {
this.#orderId = ordId;
this.total = total;
this.date = date;
}
#addTax() {
// hidden (private)
this.total += this.total * 0.2;
}
printReceipt() {
this.#addTax();
console.log(
`Receipt Id: ${this.#orderId}, Date: ${this.total} Total: ${this.date}`
);
}
}
const myOrder = new Order(1, 70, "11-30-2023");
myOrder.addTax(); // Uncaught TypeError: myOrder.addTax is not a function
myOrder.printReceipt(); // output: Receipt Id: 1, Date: 84 Total: 11-30-2023
// note that the addTax method was used internally to increase the total from 70 to 84
addTax
cannot be directly invoked, an error occurs. However, printReceipt
can invoke it internally. It is the exact same form of data hiding used with private variables simply applied to methods.
Getters and Setters
Making fields private protects the data within an object but having zero access to fields isn’t useful. We use getters and setters to provide and control access to fields. In the below example the Order class has been updated to make all fields private and provide access to them through getters and setters. All methods are made public.
class Order {
// hidden (private)
#orderId;
#total;
#date;
constructor(ordId, total, date) {
this.#orderId = ordId;
this.#total = total;
this.#date = date;
}
// Getter for orderId
get orderId() {
return this.#orderId;
}
// Setter for orderId
set orderId(newOrderId) {
this.#orderId = newOrderId;
}
// Getter for total
get total() {
return this.#total;
}
// Setter for total
set total(newTotal) {
if (newTotal >= 0) {
this.#total = newTotal;
} else {
console.log('Total cannot be negative.');
}
}
// Getter for date
get date() {
return this.#date;
}
// Setter for date
set date(newDate) {
this.#date = newDate;
}
addTax() {
// hidden (private)
this.#total += this.#total * 0.2;
}
printReceipt() {
this.addTax();
console.log(
`Receipt Id: ${this.#orderId}, Date: ${this.#date}, Total: ${this.#total}`
);
}
}
// Example usage
const order = new Order(123, 100, '2023-06-24');
console.log(order.orderId); // 123
console.log(order.total); // 100
console.log(order.date); // 2023-06-24
order.total = 120;
order.date = '2023-07-01';
order.printReceipt();
// Output: Receipt Id: 123, Date: 2023-07-01, Total: 144
Each getter and setter for a field represents a property of an object (ex. orderId
).
Static keyword
As we have seen above, access to fields and methods can limited using the private feature. In a similar way they can be made static using the static
keyword. Static methods and properties allow us to create methods and properties that are attached to the main class itself instead of the instance of a class. Therefore, these static methods and properties cannot be accessed by the instance of a class.
class User {
constructor(name) {
this.name = name;
}
// This method is available to all instances of a class
greetUser() {
console.log(`Hello ${this.name}!`);
}
static company = "Acme";
// This method is available only on the User class itself
static displayTime() {
console.log("12:00");
}
}
const newUser = new User("Ola Nordmann");
// This is called on the new instance of the class
newUser.greetUser();
// Logs:
// Hello Ola Nordmann
// The static property 'company' is only available on the class itself
console.log(User.company);
// Logs:
// Acme
// The static method 'displayTime()' is only available on the class itself
User.displayTime();
// Logs:
// 12:00
This is useful when you want to create a class just to provide some useful methods or values and not to manage data in individual objects. For example the calculator class below.
class Calculator {
static add(a, b) {
return a + b;
}
static subtract(a, b) {
return a - b;
}
static multiply(a, b) {
return a * b;
}
static divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}
static modulus(a, b) {
return a % b;
}
static power(a, b) {
return a ** b;
}
}
// Example usage
const num1 = 10;
const num2 = 5;
console.log(`Addition: ${Calculator.add(num1, num2)}`); // Addition: 15
console.log(`Subtraction: ${Calculator.subtract(num1, num2)}`); // Subtraction: 5
console.log(`Multiplication: ${Calculator.multiply(num1, num2)}`); // Multiplication: 50
console.log(`Division: ${Calculator.divide(num1, num2)}`); // Division: 2
console.log(`Modulus: ${Calculator.modulus(num1, num2)}`); // Modulus: 0
console.log(`Power: ${Calculator.power(num1, num2)}`); // Power: 100000
The benefit is that there is no need to create an instance of on object to access the methods, you simply use the class name.
Lesson task
Goal
To be able to implement information hiding on a class.
Brief
We are going to modify our previous lessons task’s class based on what you have learnt so far. Apply information hiding to the Person class.
Level 1 process
Modify the Person class to make firstName and lastName private fields.
Add a getter method getFirstName that returns the value of firstName.
Add a setter method setFirstName that allows updating the value of firstName.
Add a getter method getLastName that returns the value of lastName.
Add a setter method setLastName that allows updating the value of lastName.
Verify that the getters and setters work by creating an instance of the Person class, updating the fields using the setters, and logging the values using the getters.