Information Hiding in OOP

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

javascript
	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.

jsx
	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");
jsx
	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.

jsx
	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");
jsx
	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.

javascript
	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.

javascript
	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.

javascript
	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

  1. Modify the Person class to make firstName and lastName private fields.

  2. Add a getter method getFirstName that returns the value of firstName.

  3. Add a setter method setFirstName that allows updating the value of firstName.

  4. Add a getter method getLastName that returns the value of lastName.

  5. Add a setter method setLastName that allows updating the value of lastName.

  6. 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.

Additional resources

MDN: Classes

Javascript.info: Class