Diving into OOP Principles

In this article by Andrea Chiarelli, the author of the book Mastering JavaScript Object-Oriented Programming, we will discuss about the OOP nature of JavaScript by showing that it complies with the OOP principles. It also will explain the main differences with classical OOP. The following topics will be addressed in the article:

  • What are the principles of OOP paradigm?
  • Support of abstraction and modeling
  • How JavaScript implements Aggregation, Association, and Composition
  • The Encapsulation principle in JavaScript
  • How JavaScript supports inheritance principle
  • Support of the polymorphism principle
  • What are the differences between classical OOP and JavaScript's OOP

(For more resources related to this topic, see here.)

Object-Oriented Programming principles

Object-Oriented Programming (OOP) is one of the most popular programming paradigms. Many developers use languages based on this programming model such as C++, Java, C#, Smalltalk, Objective-C, and many other. One of the keys to the success of this programming approach is that it promotes a modular design and code reuse—two important features when developing complex software.

However, the Object-Oriented Programming paradigm is not based on a formal standard specification. There is not a technical document that defines what OOP is and what it is not. The OOP definition is mainly based on a common sense taken from the papers published by early researchers as Kristen Nygaard, Alan Kays, William Cook, and others.

An interesting discussion about various attempts to define Object-Oriented Programming can be found online at the following URL:

http://c2.com/cgi/wiki?DefinitionsForOo

Anyway, a widely accepted definition to classify a programming language as Object Oriented is based on two requirements—its capability to model a problem through objects and its support of a few principles that grant modularity and code reuse.

In order to satisfy the first requirement, a language must enable a developer to describe the reality using objects and to define relationships among objects such as the following:

  • Association: This is the object's capability to refer another independent object
  • Aggregation: This is the object's capability to embed one or more independent objects
  • Composition: This is the object's capability to embed one or more dependent objects

Commonly, the second requirement is satisfied if a language supports the following principles:

  • Encapsulation: This is the capability to concentrate into a single entity data and code that manipulates it, hiding its internal details
  • Inheritance: This is the mechanism by which an object acquires some or all features from one or more other objects
  • Polymorphism: This is the capability to process objects differently based on their data type or structure

Meeting these requirements is what usually allows us to classify a language as Object Oriented.

Is JavaScript Object Oriented?

Once we have established the principles commonly accepted for defining a language as Object Oriented, can we affirm that JavaScript is an OOP language? Many developers do not consider JavaScript a true Object-Oriented language due to its lack of class concept and because it does not enforce compliance with OOP principles.

However, we can see that our informal definition make no explicit reference to classes. Features and principles are required for objects. Classes are not a real requirement, but they are sometimes a convenient way to abstract sets of objects with common properties. So, a language can be Object Oriented if it supports objects even without classes, as in JavaScript.

Moreover, the OOP principles required for a language are intended to be supported. They should not be mandatory in order to do programming in a language. The developer can choose to use constructs that allow him to create Object Oriented code or not. Many criticize JavaScript because developers can write code that breaches the OOP principles. But this is just a choice of the programmer, not a language constraint. It also happens with other programming languages, such as C++.

We can conclude that lack of abstract classes and leaving the developer free to use or not features that support OOP principles are not a real obstacle to consider JavaScript an OOP language. So, let's analyze in the following sections how JavaScript supports abstraction and OOP principles.

Abstraction and modeling support

The first requirement for us to consider a language as Object Oriented is its support to model a problem through objects. We already know that JavaScript supports objects, but here we should determine whether they are supported in order to be able to model reality.

In fact, in Object-Oriented Programming we try to model real-world entities and processes and represent them in our software. We need a model because it is a simplification of reality, it allows us to reduce the complexity offering a vision from a particular perspective and helps us to reason about relationship among entities.

This simplification feature is usually known as abstraction, and it is sometimes considered as one of the principles of OOP. Abstraction is the concept of moving the focus from the details and concrete implementation of things to the features that are relevant for a specific purpose, with a more general and abstract approach. In other words, abstraction is the capability to define which properties and actions of a real-world entity have to be represented by means of objects in a program in order to solve a specific problem.

For example, thanks to abstraction, we can decide that to solve a specific problem we can represent a person just as an object with name, surname, and age, since other information such as address, height, hair color, and so on are not relevant for our purpose.

More than a language feature, it seems a human capability. For this reason, we prefer not to consider it an OOP principle but a (human) capability to support modeling.

Modeling reality not only involves defining objects with relevant features for a specific purpose. It also includes the definition of relationships between objects, such as Association, Aggregation, and Composition.

Association

Association is a relationship between two or more objects where each object is independent of each other. This means that an object can exist without the other and no object owns the other.

Let us clarify with an example. In order to define a parent–child relationship between persons, we can do so as follows:

function Person(name, surname) {
this.name = name;
this.surname = surname;
this.parent = null;
}

var johnSmith = new Person("John", "Smith");
var fredSmith = new Person("Fred", "Smith");

fredSmith.parent = johnSmith;

The assignment of the object johnSmith to the parent property of the object fredSmith establishes an association between the two objects. Of course, the object johnSmith lives independently from the object fredSmith and vice versa. Both can be created and deleted independently each other.

As we can see from the example, JavaScript allows to define association between objects using a simple object reference through a property.

Aggregation

Aggregation is a special form of association relationship where an object has a major role than the other one. Usually, this major role determines a sort of ownership of an object in relation to the other. The owner object is often called aggregate and the owned object is called component. However, each object has an independent life.

An example of aggregation relationship is the one between a company and its employees, as in the following example:

var company = {
    name: "ACME Inc.",
    employees: []
};

var johnSmith = new Person("John", "Smith");
var marioRossi = new Person("Mario", "Rossi");

company.employees.push(johnSmith);
company.employees.push(marioRossi);

The person objects added to employees collection help to define the company object, but they are independent from it. If the company object is deleted, each single person still lives. However, the real meaning of a company is bound to the presence of its employees.

Again, the code show us that the aggregation relationship is supported by JavaScript by means of object reference.

It is important not to confuse the Association with the Aggregation. Even if the support of the two relationships is syntactically identical, that is, the assignment or attachment of an object to a property, from a conceptual point of view they represent different situations.

Aggregation is the mechanism that allows you to create an object consisting of several objects, while the association relates autonomous objects.

In any case, JavaScript makes no control over the way in which we associate or aggregate objects between them. Association and Aggregation raise a constraint more conceptual than technical.

Composition

Composition is a strong type of Aggregation, where each component object has no independent life without its owner, the aggregate. Consider the following example:

var person = {name: "John",
              surname: "Smith",
              address: {
                street: "123 Duncannon Street",
                city: "London",
                country: "United Kingdom"
              }};

This code defines a person with his address represented as an object. The address property in strictly bound to the person object. Its life is dependent on the life of the person and it cannot have an independent life without the person. If the person object is deleted, also the address object is deleted.

In this case, the strict relation between the person and his address is expressed in JavaScript assigning directly the literal representing the address to the address property.

OOP principles support

The second requirement that allows us to consider JavaScript as an Object-Oriented language involves the support of at least three principles—encapsulation, inheritance, and polymorphism. Let analyze how JavaScript supports each of these principles.

Encapsulation

Objects are central to the Object-Oriented Programming model, and they represent the typical expression of encapsulation, that is, the ability to concentrate in one entity both data (properties) and functions (methods), hiding the internal details.

In other words, the encapsulation principle allows an object to expose just what is needed to use it, hiding the complexity of its implementation. This is a very powerful principle, often found in the real world that allows us to use an object without knowing how it internally works. Consider for instance how we drive cars. We need just to know how to speed up, brake, and change direction. We do not need to know how the car works in details, how its motor burns fuel or transmits movement to the wheels.

To understand the importance of this principle also in software development, consider the following code:

var company = {
    name: "ACME Inc.",
    employees: [],
    sortEmployeesByName: function() {...}
};

It creates a company object with a name, a list of employees and a method to sort the list of employees using their name property. If we need to get a sorted list of employees of the company, we simply need to know that the sortEmployeesByName() method accomplishes this task. We do not need to know how this method works, which algorithm it implements. That is an implementation detail that encapsulation hides to us.

Hiding internal details and complexity has two main reasons:

  • The first reason is to provide a simplified and understandable way to use an object without the need to understand the complexity inside. In our example, we just need to know that to sort employees, we have to call a specific method.
  • The second reason is to simplify change management. Changes to the internal sort algorithm do not affect our way to order employees by name. We always continue to call the same method. Maybe we will get a more efficient execution, but the expected result will not change.

We said that encapsulation hides internal details in order to simplify both the use of an object and the change of its internal implementation. However, when internal implementation depends on publicly accessible properties, we risk to frustrate the effort of hiding internal behavior. For example, what happens if you assign a string to the property employees of the object company?

company.employees = "this is a joke!";

company.sortEmployeesByName();

The assignment of a string to a property whose value is an array is perfectly legal in JavaScript, since it is a language with dynamic typing. But most probably, we will get an exception when calling the sort method after this assignment, since the sort algorithm expects an array.

In this case, the encapsulation principle has not been completely implemented. A general approach to prevent direct access to relevant properties is to replace them with methods. For example, we can redefine our company object as in the following:

function Company(name) {
    var employees = [];

    this.name = name;

        this.getEmployees = function() { 
            return employees;
            };
    this.addEmployee = function(employee) { 
            employees.push(employee);
            };
    this.sortEmployeesByName = function() {
            ...
            };
}

var company = new Company("ACME Inc.");

With this approach, we cannot access directly the employees property, but we need to use the getEmployees() method to obtain the list of employees of the company and addEmployee() to add an employee to the list. This guarantees that the internal state remains really hidden and consistent. The way we created methods for the Company() constructor is not the best one.

This is just one possible approach to enforce encapsulation by protecting the internal state of an object. This kind of data protection is usually called information hiding and, although often linked to encapsulation, it should be considered as an autonomous principle. Information hiding deals with the accessibility to an object's members, in particular to properties. While encapsulation concerns hiding details, the information hiding principle usually allows different access levels to the members of an object.

Inheritance

In Object-Oriented Programming, inheritance enables new objects to acquire the properties of existing objects. This relationship between two objects is very common and can be found in many situations in real life. It usually refers to creating a specialized object starting from a more general one.

Let's consider, for example, a person: he has some features such as name, surname, height, weight, and so on. The set of features describes a generic entity that represents a person. Using abstraction, we can select the features needed for our purpose and represent a person as an object:

 Mastering JavaScript Object-Oriented Programming

If we need a special person who is able to program computers, that is a programmer, we need to create an object that has all the properties of a generic person plus some new properties that characterize the programmer object. For instance, the new programmer object can have a property describing which programming language he knows.

Suppose we choose to create the new programmer object by duplicating the properties of the person object and adding to it the programming language knowledge as follows:

 Mastering JavaScript Object-Oriented Programming

This approach is in contrast with the Object-Oriented Programming goals. In particular, it does not reuse existing code, since we are duplicating the properties of the person object. A more appropriate approach should reuse the code created to define the person object. This is where the inheritance principle can help us. It allows to share common features between objects avoiding code duplication.

 Mastering JavaScript Object-Oriented Programming

Inheritance is also called subclassing in languages that support classes. A class that inherits from another class is called subclass, while the class from which it is derived is called superclass. Apart from the naming, the inheritance concept is the same, although of course it does not seem suited to JavaScript.

We can implement inheritance in JavaScript in various ways. Consider, for example, the following constructor of person objects:

function Person() {
this.name = "";
this.surname = "";
}

In order to define a programmer as a person specialized in computer programming, we will add a new property describing its knowledge about a programming language: knownLanguage.

A simple approach to create the programmer object that inherits properties from person is based on prototype. Here is a possible implementation:

function Programmer() {
    this.knownLanguage = "";
}

Programmer.prototype = new Person();

We will create a programmer with the following code:

var programmer = new Programmer();

We will obtain an object that has the properties of the person object (name and surname) and the specific property of the programmer (knownLanguage), that is, the programmer object inherits the person properties.

This is a simple example to demonstrate that JavaScript supports the inheritance principle of Object-Oriented Programming at its basic level. Inheritance is a complex concept that has many facets and several variants in programming, many of them dependent on the used language.

Polymorphism

In Object-Oriented Programming, polymorphism is understood in different ways, even if the basis is a common notion—the ability to handle multiple data types uniformly.

Support of polymorphism brings benefits in programming that go toward the overall goal of OOP. Mainly, it reduces coupling in our application, and in some cases, allows to create more compact code.

Most common ways to support polymorphism by a programming language include:

  • Methods that take parameters with different data types (overloading)
  • Management of generic types, not known in advance (parametric polymorphism)
  • Expressions whose type can be represented by a class and classes derived from it (subtype polymorphism or inclusion polymorphism)

In most languages, overloading is what happens when you have two methods with the same name but different signature. At compile time, the compiler works out which method to call based on matching between types of invocation arguments and method's parameters. The following is an example of method overloading in C#:

public int CountItems(int x) {
    return x.ToString().Length;
}

public int CountItems(string x) {
    return x.Length;
}

The CountItems()method has two signatures—one for integers and one for strings. This allows to count the number of digits in a number or the number of characters in a string in a uniform manner, just calling the same method.

Overloading can also be expressed through methods with different number of arguments, as shown in the following C# example:

public int Sum(int x, int y) {
    return Sum(x, y, 0);
}

public int Sum(int x, int y, int z) {
    return x+ y + z;
}

Here, we have the Sum()method that is able to sum two or three integers. The correct method definition will be detected on the basis of the number of arguments passed.

As JavaScript developers, we are able to replicate this behavior in our scripts. For example, the C# CountItems() method become in JavaScript as follows:

function countItems(x) {
return x.toString().length;
}

While the Sum() example will be as follows:

function sum(x, y, z) {
x = x?x:0;
y = y?y:0;
z = z?z:0;
return x + y + z;
}

Or, using the more convenient ES6 syntax:

function sum(x = 0, y = 0, z = 0) {
return x + y + z;
}

These examples demonstrate that JavaScript supports overloading in a more immediate way than strong-typed languages.

In strong-typed languages, overloading is sometimes called static polymorphism, since the correct method to invoke is detected statically by the compiler at compile time.

Parametric polymorphism allows a method to work on parameters of any type. Often it is also called generics and many languages support it in built-in methods. For example, in C#, we can define a list of items whose type is not defined in advance using the List<T> generic type. This allows us to create lists of integers, strings, or any other type.

We can also create our generic class as shown by the following C# code:

public class Stack<T> {
    private T[] items;
    private int count;
    public void Push(T item) { ... }
    public T Pop() { ... }
}

This code defines a typical stack implementation whose item's type is not defined. We will be able to create, for example, a stack of strings with the following code:

var stack = new Stack<String>();

Due to its dynamic data typing, JavaScript supports parametric polymorphism implicitly. In fact, the type of function's parameters is inherently generic, since its type is set when a value is assigned to it. The following is a possible implementation of a stack constructor in JavaScript:

function Stack()
{
 this.stack = [];
 this.pop = function(){
  return this.stack.pop();
 }
 this.push = function(item){
  this.stack.push(item);
 }
}

Subtype polymorphism allows to consider objects of different types, but with an inheritance relationship, to be handled consistently. This means that wherever I can use an object of a specific type, here I can use an object of a type derived from it.

Let's see a C# example to clarify this concept:

public class Person {
 public string Name {get; set;}
 public string SurName {get; set;}
}

public class Programmer:Person {
    public String KnownLanguage {get; set;}
}

public void WriteFullName(Person p) {
 Console.WriteLine(p.Name + " " + p.SurName);   
}

var a = new Person();
a.Name = "John";
a.SurName = "Smith";

var b = new Programmer();
b.Name = "Mario";
b.SurName = "Rossi";
b.KnownLanguage = "C#";

WriteFullName(a);	//result: John Smith
WriteFullName(b);	//result: Mario Rossi

In this code, we again present the definition of the Person class and its derived class Programmer and define the method WriteFullName() that accepts argument of type Person. Thanks to subtype polymorphism, we can pass to WriteFullName() also objects of type Programmer, since it is derived from Person. In fact, from a conceptual point of view a programmer is also a person, so subtype polymorphism fits to a concrete representation of reality.

Of course, the C# example can be easily reproduced in JavaScript since we have no type constraint. Let's see the corresponding code:

function Person() {
this.name = "";
this.surname = "";
}

function Programmer() {
	this.knownLanguage = "";
}

Programmer.prototype = new Person();

function writeFullName(p) {
	console.log(p.name + " " + p.surname);
}

var a = new Person();
a.name = "John";
a.surname = "Smith";

var b = new Programmer();
b.name = "Mario";
b.surname = "Rossi";
b.knownLanguage = "JavaScript";

writeFullName(a);	//result: John Smith
writeFullName(b);	//result: Mario Rossi

As we can see, the JavaScript code is quite similar to the C# code and the result is the same.

JavaScript OOP versus classical OOP

The discussion conducted so far shows how JavaScript supports the fundamental Object-Oriented Programming principles and can be considered a true OOP language as many other. However, JavaScript differs from most other languages for certain specific features which can create some concern to the developers used to working with programming languages that implement the classical OOP.

The first of these features is the dynamic nature of the language both in data type management and object creation. Since data types are dynamically evaluated, some features of OOP, such as polymorphism, are implicitly supported. Moreover, the ability to change an object structure at runtime breaks the common sense that binds an object to a more abstract entity like a class.

The lack of the concept of class is another big difference with the classical OOP. Of course, we are talking about the class generalization, nothing to do with the class construct introduced by ES6 that represents just a syntactic convenience for standard JavaScript constructors.

Classes in most Object-Oriented languages represent a generalization of objects, that is, an extra level of abstraction upon the objects.

So, classical Object-Oriented programming has two types of abstractions—classes and objects. An object is an abstraction of a real-world entity while a class is an abstraction of an object or another class (in other words, it is a generalization). Objects in classical OOP languages can only be created by instantiating classes.

JavaScript has a different approach in object management. It has just one type of abstraction—the objects. Unlike the classical OOP approach, an object can be created directly as an abstraction of a real-world entity or as an abstraction of another object. In the latter case the abstracted object is called prototype. As opposed to the classical OOP approach, the JavaScript approach is sometimes called Prototypal Object-Oriented Programming.

Of course, the lack of a notion of class in JavaScript affects the inheritance mechanism. In fact, while in classical OOP inheritance is an operation allowed on classes, in prototypal OOP inheritance is an operation on objects.

That does not mean that classical OOP is better than prototypal OOP or vice versa. They are simply different approaches. However, we cannot ignore that these differences lead to some impact in the way we manage objects. At least we note that while in classical OOP classes are immutable, that is we cannot add, change, or remove properties or methods at runtime, in prototypal OOP objects and prototypes are extremely flexible. Moreover, classical OOP adds an extra level of abstraction with classes, leading to a more verbose code, while prototypal OOP is more immediate and requires a more compact code.

Summary

In this article, we explored the basic principles of Object-Oriented Programming paradigm. We have been focusing on abstraction to define objects, association, aggregation, and composition to define relationships between objects, encapsulation, inheritance, and polymorphism principles to outline the basic principles required by OOP. We have seen how JavaScript supports all features that allows us to define it as a true OOP language and have compared classical OOP with prototypal OOP.

Once we established that JavaScript is a true Object-Oriented language like other languages such as Java, C #, and C ++.

Resources for Article:




Further resources on this subject:

  • Just Object Oriented Programming (Object Oriented Programming, explained) [article]
  • Introducing Object Oriented Programmng with TypeScript [article]
  • Python 3 Object Oriented Programming: Managing objects [article]

  • You've been reading an excerpt of:

    Mastering JavaScript Object-Oriented Programming

    Explore Title