Introducción a la programación orientada a objetos en JavaScript

Este artículo es para estudiantes de JavaScript que no tienen ningún conocimiento previo en programación orientada a objetos (POO). Me centro en las partes de OOP que solo son relevantes para JavaScript y no OOP en general. Evito el polimorfismo porque encaja mejor con un lenguaje de tipo estático.

¿Por qué necesitas saber esto?

¿Ha elegido JavaScript para ser su primer lenguaje de programación? ¿Quiere ser un desarrollador de alta tecnología que trabaje en sistemas empresariales gigantes que abarcan cien mil líneas de código o más?

A menos que aprenda a adoptar plenamente la programación orientada a objetos, estará realmente perdido.

Diferentes mentalidades

En el fútbol, ​​puedes jugar desde una defensa segura, puedes jugar con balones altos desde los lados o puedes atacar como si no hubiera un mañana. Todas estas estrategias tienen el mismo objetivo: ganar el juego.

Lo mismo ocurre con los paradigmas de programación. Hay diferentes formas de abordar un problema y diseñar una solución.

La programación orientada a objetos, o POO, es EL paradigma para el desarrollo de aplicaciones modernas. Es compatible con los principales lenguajes como Java, C # o JavaScript.

El paradigma orientado a objetos

Desde la perspectiva de la programación orientada a objetos, una aplicación es una colección de "objetos" que se comunican entre sí. Basamos estos objetos en cosas del mundo real, como productos en inventario o registros de empleados. Los objetos contienen datos y realizan alguna lógica basada en sus datos. Como resultado, el código OOP es muy fácil de entender. Lo que no es tan fácil es decidir cómo dividir una aplicación en estos pequeños objetos en primer lugar.

Si eras como yo cuando lo escuché por primera vez, no tienes ni idea de lo que esto realmente significa: todo suena muy abstracto. Sentirse de esa manera está absolutamente bien. Es más importante que haya escuchado la idea, la recuerde e intente aplicar OOP en su código. Con el tiempo, adquirirá experiencia y alineará más de su código con este concepto teórico.

Lección : la programación orientada a objetos basada en objetos del mundo real permite que cualquiera lea su código y comprenda lo que está sucediendo.

Objeto como pieza central

Un ejemplo sencillo le ayudará a ver cómo JavaScript implementa los principios fundamentales de la programación orientada a objetos. Considere un caso de uso de compras en el que coloca productos en su carrito y luego calcula el precio total que debe pagar. Si toma su conocimiento de JavaScript y codifica el caso de uso sin OOP, se vería así:

const bread = {name: 'Bread', price: 1};const water = {name: 'Water', price: 0.25};
const basket = [];basket.push(bread);basket.push(bread);basket.push(water);basket.push(water);basket.push(water);
const total = basket .map(product => product.price) .reduce((a, b) => a + b, 0);
console.log('one has to pay in total: ' + total);

La perspectiva OOP hace que escribir mejor código sea más fácil porque pensamos en los objetos como los encontraríamos en el mundo real. Como nuestro caso de uso contiene una canasta de productos, ya tenemos dos tipos de objetos: el objeto de la canasta y los objetos del producto.

La versión OOP del caso de uso de compras se podría escribir como:

const bread = new Product('bread', 1);const water = new Product('water', .25)const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.printShoppingInfo();

Como puede ver en la primera línea, creamos un nuevo objeto utilizando la palabra clave newseguida del nombre de lo que se llama una clase (que se describe a continuación). Esto devuelve un objeto que almacenamos en la variable bread. Repetimos eso para la variable agua y tomamos un camino similar para crear una canasta variable. Una vez que haya agregado estos productos a su carrito, finalmente imprima el monto total que debe pagar.

La diferencia entre los dos fragmentos de código es obvia. La versión OOP casi se lee como oraciones reales en inglés y puedes saber fácilmente lo que está sucediendo.

Lección : Un objeto modelado sobre cosas del mundo real consta de datos y funciones.

Clase como plantilla

Usamos clases en OOP como plantillas para crear objetos. Un objeto es una "instancia de una clase" y la "instanciación" es la creación de un objeto basado en una clase. El código está definido en la clase, pero no se puede ejecutar a menos que esté en un objeto activo.

Puede mirar clases como los planos de un automóvil. Definen las propiedades del automóvil como el par y los caballos de fuerza, funciones internas como las relaciones aire-combustible y métodos de acceso público como el encendido. Sin embargo, solo cuando una fábrica crea una instancia del automóvil, puede girar la llave y conducir.

En nuestro caso de uso, usamos la clase Product para instanciar dos objetos, pan y agua. Por supuesto, esos objetos necesitan código que debes proporcionar en las clases. Dice así:

function Product(_name, _price) { const name = _name; const price = _price;
this.getName = function() { return name; };
this.getPrice = function() { return price; };}
function Basket() { const products = [];
this.addProduct = function(amount, product) { products.push(...Array(amount).fill(product)); };
this.calcTotal = function() { return products .map(product => product.getPrice()) .reduce((a, b) => a + b, 0); };
this.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal()); };}

Una clase en JavaScript parece una función, pero la usa de manera diferente. El nombre de la función es el nombre de la clase y está en mayúscula. Como no devuelve nada, no llamamos a la función de la forma habitual como const basket = Product('bread', 1);. En su lugar, agregamos la palabra clave new like const basket = new Product('bread', 1);.

El código dentro de la función es el constructor. Este código se ejecuta cada vez que se crea una instancia de un objeto. El producto tiene los parámetros _namey _price. Cada nuevo objeto almacena estos valores en su interior.

Además, podemos definir funciones que proporcionará el objeto. Definimos estas funciones anteponiendo la palabra clave this que las hace accesibles desde el exterior (ver Encapsulación). Observe que las funciones tienen acceso completo a las propiedades.

Class Basket doesn’t require any arguments to create a new object. Instantiating a new Basket object simply generates an empty list of products that the program can fill afterwards.

Lesson: A class is a template for generating objects during runtime.

Encapsulation

You may encounter another version of how to declare a class:

function Product(name, price) { this.name = name; this.price = price;}

Mind the assignment of the properties to the variable this. At first sight, it seems to be a better version because it doesn’t require the getter (getName & getPrice) methods anymore and is therefore shorter.

Unfortunately, you have now given full access to the properties from the outside. So everybody could access and modify it:

const bread = new Product('bread', 1);bread.price = -10;

This is something you don’t want as it makes the application more difficult to maintain. What would happen if you added validation code to prevent, for example, prices less than zero? Any code that accesses the price property directly would bypass the validation. This could introduce errors that would be difficult to trace. Code that uses the object’s getter methods, on the other hand, is guaranteed to go through the object’s price validation.

Objects should have exclusive control over their data. In other words, the objects “encapsulate” their data and prevent other objects from accessing the data directly. The only way to access the data is indirect via the functions written into the objects.

Data and processing (aka logic) belong together. This is especially true when it comes to larger applications where it is very important that processing data is restricted to specifically-defined places.

Done right, OOP produces modularity by design, the holy grail in software development. It keeps away the feared spaghetti-code where everything is tightly coupled and you don’t know what happens when you change a small piece of code.

In our case, objects of class Product don’t let you change the price or the name after their initialization. The instances of Product are read-only.

Lesson: Encapsulation prevents access to data except through the object’s functions.

Inheritance

Inheritance lets you create a new class by extending an existing class with additional properties and functions. The new class “inherits” all of the features of its parent, avoiding the creation of new code from scratch. Furthermore, any changes made to the parent class will automatically be available to the child class. This makes updates much easier.

Let’s say we have a new class called Book that has a name, a price and an author. With inheritance, you can say that a Book is the same as a Product but with the additional author property. We say that Product is the superclass of Book and Book is a subclass of Product:

function Book(_name, _price, _author) { Product.call(this, _name, _price); const author = _author; this.getAuthor = function() { return author; }}

Note the additional Product.call along the this as the first argument. Please be aware: Although book provides the getter methods, it still doesn’t have direct access to the properties name and price. Book must call that data from the Product class.

You can now add a book object to the basket without any issues:

const faust = new Book('faust', 12.5, 'Goethe');basket.addProduct(1, faust);

Basket expects an object of type Product. Since book inherits from Product through Book, it is also a Product.

Lesson: Subclasses can inherit properties and functions from superclasses while adding properties and functions of their own.

JavaScript and OOP

You will find three different programming paradigms used to create JavaScript applications. They are Prototype-Based Programming, Object-Oriented Programming and Functional-Oriented Programming.

The reason for this lies in JavaScript’s history. Originally, it was prototype-based. JavaScript was not intended as a language for large applications.

Against the plan of its founders, developers increasingly used JavaScript for bigger applications. OOP was grafted on top of the original prototype-based technique.

The prototype-based approach is shown below. It is seen as the “classical and default way” to construct classes. Unfortunately it does not support encapsulation.

Even though JavaScript’s support for OOP is not at the same level as other languages like Java, it is still evolving. The release of version ES6 added a dedicated class keyword we could use. Internally, it serves the same purpose as the prototype property, but it reduces the size of the code. However, ES6 classes still lack private properties, which is why I stuck to the “old way”.

For the sake of completeness, this is how we would write the Product, Basket and Book with ES6 classes and also with the prototype (classical and default) approach. Please note that these versions don’t provide encapsulation:

// ES6 version
class Product { constructor(name, price) { this.name = name; this.price = price; }}
class Book extends Product { constructor(name, price, author) { super(name, price); this.author = author; }}
class Basket { constructor() { this.products = []; }
 addProduct(amount, product) { this.products.push(…Array(amount).fill(product)); }
 calcTotal() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0); }
 printShoppingInfo() { console.log('one has to pay in total: ' + this.calcTotal()); }}
const bread = new Product('bread', 1);const water = new Product('water', 0.25);const faust = new Book('faust', 12.5, 'Goethe');
const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.addProduct(1, faust);basket.printShoppingInfo();
//Prototype versionfunction Product(name, price) { this.name = name; this.price = price;}function Book(name, price, author) { Product.call(this, name, price); this.author = author;}Book.prototype = Object.create(Product.prototype);Book.prototype.constructor = Book;function Basket() { this.products = [];}Basket.prototype.addProduct = function(amount, product) { this.products.push(...Array(amount).fill(product));};Basket.prototype.calcTotal = function() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0);};Basket.prototype.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal());};

Lesson: OOP was added to JavaScript later in its development.

Summary

As a new programmer learning JavaScript, it will take time to appreciate Object-Oriented Programming fully. The important things to understand at this early stage are the principles the OOP paradigm is based on and the benefits they provide:

  • Objects modeled on real-world things are the centerpiece of any OOP-based application.
  • Encapsulation protects data from uncontrolled access.
  • Objects have functions that operate on the data the objects contain.
  • Classes are the templates used to instantiate objects.
  • Inheritance is a powerful tool for avoiding redundancy.
  • OOP is more verbose but easier to read than other coding paradigms.
  • Since OOP came later in JavaScript’s development, you may come across older code that uses prototype or functional programming techniques.

Further reading

  • //developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
  • //voidcanvas.com/es6-private-variables/
  • //medium.com/@rajaraodv/is-class-in-es6-the-new-bad-part-6c4e6fe1ee65
  • //developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance

    * //en.wikipedia.org/wiki/Object-oriented_programming

  • //en.wikipedia.org/wiki/Object-oriented_programming