Design Development JavaScript

4 Core Principles of Object-oriented Programming in JavaScript

There are four core principles in object-oriented programming. Without them programming language can’t be called object-oriented. These principles are encapsulation, inheritance, polymorphism and abstraction. In this article, you will learn about these principles, their meaning, and how to use them.

Encapsulation

The first of the four core principles in object-oriented programming is encapsulation. The idea of encapsulation is that implementation details should not be visible to end users. For example, let’s say you have a class. Implementing the principle of encapsulation would mean that all properties of this class are private, hidden from other classes.

The only way to access these class properties would be through public accessor methods of that class. Accessor method is a method created for the purpose of accessing specific class property. This practice of hiding information or data about implementation is called “data hiding”.

To implement encapsulation in JavaScript, we create new class. Inside it, we declare two new properties, also called fields and members. We make all of them private. This will ensure all these properties are hidden. They will be inaccessible from the outside of the class. From now, the only way to access them is through methods inside that class.

This is the next thing we will do. We will create public setter and getter methods for each private property. These methods will allow us to view and modify values of these properties.

class User {
  // Create private class properties/fields
  // NOTE: Private fields was added to JavaScript in ES2015
  #_username
  #_email

  // Create getter method
  // to get username property
  get username() {
    return this.#_username
  }

  // Create setter method
  // to set, or change, username property
  set username(newUsername) {
    if (newUsername && newUsername.length === 0) {
      throw new Error('username must contain more than 0 characters.')
    }

    this.#_username = newUsername
  }

  // Create getter method
  // to get email property
  get email() {
    return this.#_email
  }

  // Create setter method
  // to set, or change, email property
  set email(newEmail) {
    if (newEmail && newEmail.length === 0) {
      throw new Error('email must contain more than 0 characters.')
    }

    this.#_email = newEmail
  }
}

// Create new instance of User class
let bob = new User()

// Set username
// This invokes username setter method
bob.username = 'bobby'

// Set email
// This invokes email setter method
bob.email = 'bobby@email.com'

// Access username
// This invokes username getter method
console.log(bob.username)
// 'bobby'

// Access username
// This invokes email getter method
console.log(bob.email)
// 'bobby@email.com'

In the example above, you have a class with two private properties. These properties are username and email. Next, you have one getter and one setter method for each of these properties. Getter method starts with keyword get and setter with keyword set. When you try to access one of these properties specific getter method is invoked.

This is what happens when you access the values of these properties, using bob.username and bob.email at the bottom. When you try to change any of these properties it will invoke specific setter method. For example, when you set the value of username and email, bob.username = 'bobby' and bob.email = 'bobby@email.com'.

Thanks to this implementation, fields username and email are private. The only way to access them or change them is through the setter and getter methods you’ve created. This gives you greater control over how data is accessed or modified and more flexibility to make changes.

Inheritance

Inheritance is of the most used principles of object-oriented programming. This makes sense. Objects in the real world are often very similar. They share many of attributes and behaviors. For example, dog and cat are both animals. They both have four legs. They both can walk and speak, in some sense.

Inheritance allows you to extract these shared attributes and behaviors into a separate class. This helps you avoid writing the same code over again and again. Instead, you can let other classes “inherit” from this separate class. When this happens, the class other class(es) inherits from is called “parent class” or “superclass”.

Classes that inherit from this “parent” class are called “child classes”, “subclasses” or “derived” classes. When some class (child class) inherits from another class (parent class), it inherits all of the parent’s properties and methods. One exception are private properties and methods.

Another exception is constructor method. The constructor is not a normal class method and is not inherited by child classes. When you instantiate the parent class, the constructor method of the parent class will be called. When you want to let one class inherit from another use the extends keyword followed by the parent’s class name.

// Create parent class Animal
// This class contains shared properties and methods
class Animal {
  // Add some shared properties
  constructor(species, numOfLegs, sound) {
    this.species = species
    this.numOfLegs = numOfLegs
    this.sound = sound
  }

  // Add shared method
  speak() {
    return this.sound
  }
}

// Create Dog child class
// and let it inherit from Animal class
class Dog extends Animal {
  // Add some code specific for Dog class
  constructor(species, numOfLegs, sound, canRetrieve) {
    // Use super() to call parent's class constructor
    // before accessing 'this'
    // pass only arguments defined in parent class
    super(species, numOfLegs, sound)

    this.canRetrieve = canRetrieve
  }
}

// Create Cat child class
// and let it inherit from Animal class
class Cat extends Animal {
  // Add some code specific for Cat class
  constructor(species, numOfLegs, sound, canClimbTrees) {
    // Use super() to call parent's class constructor
    // before accessing 'this'
    // pass only arguments defined in parent class
    super(species, numOfLegs, sound)

    this.canClimbTrees = canClimbTrees
  }
}

// Create instance of Dog class
const charlie = new Dog('Dog', 4, 'Bark', true)

// Create instance of Cat class
const kitty = new Cat('Cat', 4, 'Mew', true)

// Let charlie speak
charlie.speak()
// Bark

// Can charlie retrieve a ball?
charlie.canRetrieve
// true

// Can charlie climb trees?
// This will not work because canClimbTress
// is not implemented neither in parent class Animal nor in Dog class
charlie.canClimbTrees
// undefined

// Let kitty speak
kitty.speak()
// Meow

// Can charlie climb trees?
kitty.canClimbTrees
// true

// Can kitty retrieve a ball?
// This will not work because canRetrieve
// is not implemented neither in parent class Animal nor in Cat class
kitty.canRetrieve
// undefined

In the example above, you have one parent class Animal. This class contains properties and method it can share with child classes. Next, you have two child classes, Dog and Cat. These classes inherit properties and method defined in Animal. This inheritance is defined by using the extends keyword.

Along with this, each child class also implements additional property. In case of Dog class, this unique property is canRetrieve. For Cat class, it is canClimbTrees. These two unique properties are available only for instance of that specific class. They are not available for other classes, if they don’t inherit from Dog and Cat.

Polymorphism

Polymorphism is the third of principles of object-oriented programming. The word “polymorphism” means having “many forms” or “shapes”. You know about the principle of inheritance and how it works. About polymorphism. Let’s say you have a couple of classes related to each other through inheritance, parent class and child classes.

In order for polymorphism to occur two things have to happen. First, one of these child classes creates its own method. Second, this method in some way overrides a method with the same name that is declared in parent’s class. For example, let’s say you have a class Dog and Cat. Both inherit from the Animal class.

The Animal class has speak() method. Both child classes Dog and Cat also has their own implementation of speak() method. In both cases, this method returns a different result.

// Create parent class Animal
class Animal {
  // Add shared speak method
  speak() {
    return 'Grrr.'
  }
}

// Create class Dog, child class of Animal
class Dog extends Animal {
  // Create new implementation of speak() method
  // This is polymorphism
  speak() {
    return 'Woof, woof.'
  }
}

// Create class Cat, child class of Animal
class Cat extends Animal {
  // Create new implementation of speak() method
  // This is polymorphism
  speak() {
    return 'Meow.'
  }
}

// Create instance of Dog class
const myDog = new Dog()

// Call the speak method on Dog instance
myDog.speak()
// Woof, woof.

// Create instance of Cat class
const myCat = new Cat()

// Call the speak method on Cat instance
myCat.speak()
// Meow.

Method overriding

Polymorphism can occur in two ways. The first way is what you saw in the previous example. It is when a subclass implements its own version of a method that was inherited from its parent class. This type of polymorphism is also called “method overriding”, or “runtime polymorphism”.

// Create parent class Animal
class Animal {
  // Add shared speak method
  speak() {
    return 'Grrr.'
  }
}

// Create class Dog, child class of Animal
class Dog extends Animal {
  // Create new implementation of speak() method
  // This method overriding
  speak() {
    return 'Woof, woof.'
  }
}

In the example above, you have a Cat class that overrides the speak() method it inherited from parent class Animal. As a result, the speak() method will now work differently for the Cat class and all its child classes. When it comes to method overriding there are two good practices to follow.

First, new implementation of a method should have the same return type and arguments. If inherited method returns a string new implementation should also return a string. Second, the method’s access level should not be more restrictive than the overridden method’s access level.

For example, if the parent’s class method is declared as public, the overriding method in the child class should be public as well. It should not be private.

One more thing. When you have a method that is static it can’t be overridden. The same applies to methods that can’t be inherited. If a method can’t be inherited, it can’t be overridden. This also means that you can override constructor methods.

Method overloading

The second way in which polymorphism can occur is method overloading. This is also called “compile-time polymorphism”. This is when two methods have the same name, but different parameters. Method overloading can happen in two ways. First, the number of parameters changes. For example, the overriding method adds new parameters or removes exist.

Second, the type of parameters changes. The inherited method takes a parameter of type number as a parameter. Then, you create overriding method that takes a parameter of type string as a parameter instead.

// Create parent class Animal
class Person {
  // Create sayNumber() method that accepts integer
  sayNumber(num) {
    return `The number is ${num}.`
  }
}

// Create class Dog, child class of Animal
class Boy extends Person {
  // Create new implementation of sayNumber() method
  // and make it so it accepts two parameters
  // This when method overloading
  sayNumber(numOne, numTwo) {
    return `The number is ${numOne} and ${numTwo}.`
  }
}


// Create instance of Person class
const jack = new Person()

// Call sayNumber() method
jack.sayNumber(14)
// The number is 14.


// Create instance of Boy class
const tony = new Boy()

// Call sayNumber() method
tony.sayNumber(13, 17)
// The number is 13 and 17.

Abstraction

The last of the principles of object-oriented programming is abstraction. The idea behind this principle is that the outside world should be provided with only essential information about an object. It should provide only information relevant for using it. It should not provide information about implementation details of this object.

Think about a kettle. There is a lot going on when you use it to warm water. However, you don’t need to know any of that. All you need to know is how to fill it with water and which button to press. The rest of information stays hidden under the hood.

Another way to think about abstraction is to think about focusing on essential qualities, rather than the characteristics of one specific example. In case of classes, abstraction can be achieved by creating an abstract class. This class is special. It can’t be instantiated. When you want to use it, you have to let another class inherit from it.

Only abstract classes can contain abstract methods. Other classes can’t. If class contains abstract method, it should be also abstract. Abstract methods are methods that are declared without any implementation. They are like a placeholder. The implementation is left for child classes that inherit from the abstract class.

In TypeScript, you can define abstract class using abstract keyword. When you want to declare abstract method you also use the abstract keyword. Now, you can create new normal class and let it inherit from the abstract. In this child class, you can implement the method(s) you declared as abstract in parent abstract class.

// Create abstract class
abstract class Person {
  constructor(public name: string) {}

  // Create abstract sayHi() method without implementation
  abstract sayHi(): void
}

// Create class Man that inherits from abstract class Person
class Man extends Person {
  // Implement its own constructor
  constructor(name: string) {
    super(name)

    this.name = name
  }

  // Implement abstract sayHi() method
  sayHi() {
    return `Hi, my name is ${this.name}.`
  }
}


// Create instance of Man class
const joel = new Man('Joel')

// Call newly implemented sayHi() method
joel.sayHi()
// Hi, my name is Joel.

In the example above, you define an abstract class Person. Inside this class, you define abstract method sayHi(). Next, you create new child class Man and let it inherit from class Person. Inside the Man class you implement the sayHi() method. Meaning, you actually specify what it is supposed to do.

JavaScript doesn’t have a native support for abstract keyword. However, the TypeScript example above can be re-written to plain JavaScript. You can do that by using regular classes with throw statements. These statements will ensure the pseudo-abstract class will not be instantiated and its pseudo-abstract methods implemented.

// Create a pseudo-abstract class
class Person {
  constructor(name) {
    // Make sure class can't be instantiated
    if (this.constructor === Person) {
      throw new Error('Abstract classes can\'t be instantiated.')
    }
  }

  // Create pseudo-abstract sayHi() method
  sayHi() {
    // Make sure sayHi() method must be implemented in child class
    throw new Error('Method \'sayHi()\' must be implemented.')
  }
}

// Create class Man that inherits from pseudo-abstract class Person
class Man extends Person {
  // Implement its own constructor
  constructor(name) {
    super(name)

    this.name = name
  }

  // Implement pseudo-abstract sayHi() method
  sayHi() {
    return `Hi, my name is ${this.name}.`
  }
}

// Create class Woman that inherits from pseudo-abstract class Person
class Woman extends Person {
  // Implement its own constructor
  constructor(name) {
    super(name)

    this.name = name
  }

  // Forget to implement pseudo-abstract sayHi() method
}


// Create instance of Man class
const saed = new Man('Saed')

// Call newly implemented sayHi() method
saed.sayHi()
// Hi, my name is Saed.


// Create instance of Woman class
const julii = new Woman('Julii')

// Call newly implemented sayHi() method
julii.sayHi()
// Method 'sayHi()' must be implemented.


// Try to create instance of abstract class Person
const tom = new Person('Tom')
// Abstract classes can't be instantiated.

Abstract classes can be quite useful. You might have some classes that share some methods, but each uses these methods in their own way. With abstract class, you can declare this method. You can only “say” that there is this and this method. That’s it. You leave implementation of this method to every child class.

Conclusion: 4 core principles of object-oriented programming in JavaScript

You did it! If you followed along with me through this article you should have a good understanding of the four core principles of object-oriented programming. You should know what encapsulation, inheritance, polymorphism and abstraction are about and how to use each of them in your projects.

I hope you enjoyed this article and have fun while you followed it. Please let me know what you think and share the article if it helped you out, and donations are always welcome!

If you liked this article, please subscribe so you don't miss any future post.

If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.