Programación orientada a objetos
Introducción
La programación orientada a objetos (también conocida por las siglas: POO) es uno de los paradigmas de desarrollo más utilizados para la creación de aplicaciones. En los primeros tiempos de la programación, el software se creaba a partir de secuencias de instrucciones que se ejecutaban una tras otra; esto es lo que llamamos programación imperativa. Con el tiempo, los programas informáticos se han vuelto más complejos, lo que ha aumentado considerablemente la dificultad de mantenerlos y hacer que evolucionen. En la década de 1970, Alan Kay sentó las bases de la programación orientada a objetos. Este paradigma se hizo popular rápidamente y todavía se utiliza en la actualidad como base de muchos de los programas de software que utilizamos todos los días. Este estilo de programación permite organizar el software usando subconjuntos más pequeños, comúnmente llamados objetos. Cada uno de estos objetos contiene sus propios datos y lógica. Por lo tanto, los objetos son partes autónomas de un programa y tienen interacciones entre sí para que funcione.
TypeScript es uno de los llamados lenguajes multiparadigma. Por tanto, permite desarrollar aplicaciones utilizando programación imperativa, orientada a objetos o incluso funcional (consulte el capítulo TypeScript y la programación funcional)....
Las clases
El primer concepto importante que se debe aprender al empezar con la programación orientada a objetos es el concepto de clase. Cada objeto debe ser creado por una clase. Esto se puede comparar con las instrucciones de fabricación que contienen toda la información necesaria para la creación de un objeto. Una vez definida, la clase se utiliza en el programa para crear un objeto; esto se denomina «instancia de clase». Puede crear tantos objetos como desee para que un programa funcione.

Posteriormente, los objetos interactuarán entre sí para que el programa funcione.
Para declarar una clase en TypeScript, es necesario usar la palabra clave class, darle un nombre y abrir las llaves para definir sus características.
El concepto de clase no es nuevo en JavaScript. La palabra clave class es un azúcar sintáctico basado en la noción de prototipo en el lenguaje (consulte el capítulo Tipos e instrucciones básicas). En ECMAScript 5, es posible definir una clase mediante una función constructora. Esta tiene la particularidad de definir la estructura de los objetos y crearlos. Desde ECMAScript 2015, se prefieren las clases a las funciones constructoras porque estas últimas tienen una sintaxis poco intuitiva.
Sintaxis:
class ClassName {
// ...
}
Ejemplo:
class Employee {
// ...
}...
Propiedades
Dado que un objeto debe funcionar de forma autónoma, es necesario que mantenga un estado durante su uso. Este estado está contenido en el objeto en forma de datos. Por tanto, el estado de un programa en programación orientada a objetos corresponderá al conjunto de estados de cada instancia utilizada para hacerlo funcionar.
Para asignar datos a un objeto en TypeScript, se debe utilizar una propiedad. Estas se definen a nivel de clase y deben tiparse. Para declarar una propiedad, es suficiente con agregarla entre las llaves de la clase, dándole un nombre y luego especificando su tipo.
Sintaxis:
class ClassName {
propertyName: type;
}
Ejemplo:
class Employee {
firstName: string;
lastName: string;
}
Una vez creada la instancia de una clase, es posible asignar valores a las diferentes propiedades, pero también recuperarlos. Las propiedades están disponibles utilizando un «.» después del nombre de la variable que contiene el objeto.
Ejemplo:
let employee = new Employee();
employee.firstName = "Evelyn";
employee.lastName = "Miller";
// Log: Evelyn
console.log(employee.firstName);
// Log: Miller
console.log(employee.lastName);
employee = new Employee(); ...
Métodos
Los datos contenidos en un objeto se pueden utilizar posteriormente durante la ejecución de reglas lógicas dentro del programa. Es posible escribir estas reglas de forma imperativa o utilizando una función.
Por su parte, la programación orientada a objetos propone definir esta lógica directamente en clases usando métodos. Para definir un método en una clase, es suficiente con agregar una función dentro de ella.
Sintaxis:
class ClassName {
methodName(param1: type, param2: type, ...): type {
// ...
}
}
Dentro del método, será posible hacer referencia a la instancia actual de la clase usando palabra clave this. Esto le permite manipular las propiedades de un objeto o utilizar otro método.
Ejemplo:
class Employee {
firstName!: string;
lastName!: string;
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
const employee = new Employee();
employee.firstName = "Evelyn";
employee.lastName = "Miller";
const fullName = employee.getFullName();
// Log: Evelyn Miller
console.log(fullName);
La palabra clave this ya se ha analizado anteriormente (consulte el capítulo Tipos e instrucciones básicas). Aunque su uso es diferente en la programación orientada a objetos, las reglas básicas relacionadas con el alcance de this se aplican siempre. Cuidado al usarlo en una función devuelta por un método. Siempre es necesario utilizar una función bind o flecha para aplicar el alcance correcto y no causar un error al ejecutar la función.
Al igual que las funciones, los métodos pueden tener parámetros los cuales cumplen todas las reglas relativas a las funciones vistas anteriormente (consulte el capítulo...
Constructores
En la sección Propiedades de este capítulo, la inicialización de propiedades se realizó con valores predeterminados. Existe una forma más elegante de establecer estos valores al crear una instancia de clase: los constructores.
Un constructor es similar a un método y se llamará automáticamente al crear una instancia de una clase con la palabra clave new. Para definir un constructor, se debe agregarlo a la clase usando la palabra clave constructor. La implementación del constructor es similar a la de un método, pero no es posible definir un nombre ni un tipo de retorno para él.
Sintaxis:
class ClassName {
constructor(param1: type, param2: type, ...) {
// ...
}
}
Ejemplo:
class Employee {
firstName: string;
lastName: string;
constructor() {
this.firstName = "Evelyn";
this.lastName = "Miller";
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
const employee = new Employee(); ...
Métodos estáticos
No siempre es necesario que un método manipule los datos contenidos en un objeto. En ese caso, parece superfluo crear una instancia de una clase para ejecutar un método que, al final, no manipulará nada a nivel de objeto. Una función simple puede ser suficiente en este tipo de casos, pero, si queremos preservar la organización de un programa desarrollado con programación orientada a objetos, otra solución es posible: métodos estáticos.
Un miembro de una clase puede declararse estático utilizando la palabra clave static. Luego se vuelve accesible directamente desde la clase en lugar de desde una instancia de la clase. Entonces no es necesario crear una instancia para utilizar el método.
Sintaxis:
class ClassName {
static propertyName: type;
static methodName(param1: type, param2: type, ...): type {
// ...
}
}
Ejemplo:
class Employee {
static getFullName(
firstName: string,
lastName: string
): string {
return `${firstName} ${lastName}`;
}
}
const fullName = Employee.getFullName("Evelyn"...
Modificadores de acceso
En todos los ejemplos vistos anteriormente, cuando se crea una instancia de una clase, todos los miembros que la componen están disponibles desde fuera del objeto. Por eso decimos que el alcance de los miembros del objeto es público.
En TypeScript, el concepto de accesibilidad de miembros es más amplio. Es posible definir diferentes niveles a los miembros que componen un objeto, con el fin de especificar cómo otros objetos interactuarán con él.
El primer nivel que se puede utilizar en TypeScript es: público. Cuando un miembro de una clase está marcado con la palabra clave public, a continuación se vuelve accesible desde fuera del objeto.
Sintaxis:
class ClassName {
public propertyName: type;
public methodName(param1: type, param2: type, ...): type {
// ...
}
}
Ejemplo:
class Employee {
public firstName: string;
public lastName: string;
public constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
public getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
const employee = new Employee("Evelyn", "Miller");
// Log: Evelyn Miller
console.log(employee.getFullName());
employee.firstName = "John";
employee.lastName = "Riley";
// Log: John Riley
console.log(employee.getFullName());
Los miembros de una clase en TypeScript son públicos de forma predeterminada, por lo que no es necesario utilizar la palabra clave public para definir el alcance de un miembro, ya que está implícita. Sin embargo, algunos desarrolladores prefieren especificarlo explícitamente para mejorar la legibilidad del código. En el resto de este capítulo, el alcance público siempre se utilizará de forma implícita.
En contraposición...
Encapsulación
La encapsulación es el primer pilar de la programación orientada a objetos. Este principio establece que es mejor controlar la modificación del estado de un objeto desde el interior que desde el exterior. Su implementación implica que las propiedades no deben exponerse públicamente, sino encapsularse. Luego, cada propiedad será aislada y, por lo tanto, deberá declararse con un alcance privado.
Para poder manipularse desde el exterior, se pueden implementar métodos a fin de obtener o redefinir el valor de la propiedad.
Ejemplo:
class Employee {
#salary: number = 0;
getSalary() {
return this.#salary;
}
setSalary(salary: number) {
const isNegative = salary < 0;
if (!isNegative) {
this.#salary = salary;
}
}
}
const employee = new Employee();
employee.setSalary(-2000);
// Log: 0
console.log(employee.getSalary());
employee.setSalary(2000);
// Log: 2000
console.log(employee.getSalary());...
Herencia
La herencia es el segundo pilar de la programación orientada a objetos. Este es un principio particularmente útil que permite que una clase recupere las características de otra clase cuando hereda de ella. Por lo tanto, en TypeScript, una clase puede heredar de otra mediante la palabra clave extends.
Sintaxis:
class Class1 extends Class2 {
//...
}
Cuando una clase hereda de otra clase, se vuelve compatible con el tipo de clase en la que se basa. Esto permite manipular una clase derivada como si fuera del tipo de la clase base de la que hereda. Esta particularidad es fundamental para establecer el concepto de polimorfismo que se abordará más adelante en este capítulo (consulte la sección Polimorfismo).
Ejemplo:
class Person {
constructor(
public firstName: string,
public lastName: string
) {}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
class Employee extends Person {
salary!: number;
}
const employee = new Employee("Evelyn", "Miller");
employee.salary = 2000;
// Log: Evelyn Miller
console.log(employee.getFullName());
Cuando el tipo derivado se manipula como tipo base, ya no es posible acceder a sus propios miembros.
Ejemplo:
const person: Person = new Employee("John", "Riley");
// Compilation Error TS2339:
// Property 'salary' does not exist on type 'Person'.
person.salary = 2000;
Una vez que una clase hereda de otra, recupera todas sus características. Por tanto, es posible, desde la clase derivada, manipular los miembros definidos en la clase base con la palabra clave this.
Ejemplo:
class Person {
constructor(
public firstName: string,
public lastName: string
)...
Abstracción
En programación orientada a objetos, una clase generalmente representa un objeto del mundo real (ejemplo: Empleado, Perro, Avión, MicroControlador…). Sin embargo, ciertos objetos pueden ser de naturaleza inmaterial y, por tanto, no tener representación en el mundo real (ejemplo: Persona, Animal, Vehículo, Circuito…). Por tanto, este tipo de objetos se consideran abstractos y solo tienen sentido en un programa si son heredados y completados por otra clase.
En TypeScript, es posible crear una clase abstracta usando la palabra clave abstract. Dada su naturaleza, no se pueden crear instancias de estas clases. Por lo tanto, es necesario utilizar la herencia para crear una clase concreta que luego será instanciable.
Sintaxis:
abstract class ClassName {
// ...
}
Ejemplo:
abstract class Person {
constructor(
public firstName: string,
public lastName: string
) {}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
// Compilation Error TS2511:
// Cannot create an instance of an abstract class.
const person = new Person("Evelyn"...
Interfaces
En la programación orientada a objetos, el concepto de interfaz le permite definir abstracciones sin necesidad de escribir clases. Una interfaz contiene una descripción de lo que implementará una clase (a veces llamado contrato). La naturaleza de las interfaces significa que los miembros descritos en su interior son públicos y, por lo tanto, cualquier otro objeto puede acceder a ellos, siempre que se refiera a la interfaz.
Para declarar una interfaz en TypeScript, debes usar la palabra clave interface, darle un nombre y abrir las llaves para definir sus características.
Sintaxis:
interface InterfaceName {
// ...
}
Ejemplo:
interface Person {
// ...
}
El concepto de interfaz no existe en ECMAScript, razón por la cual el compilador TypeScript no genera código JavaScript durante la transpilación. Por tanto, es importante tener en cuenta que este concepto solo es útil en la compilación. Se utiliza principalmente para el sistema de tipos del lenguaje.
Sintácticamente, una interfaz se parece a una clase. Sin embargo, a diferencia de estas últimas, las interfaces definen miembros, pero no su implementación. Por lo tanto, los métodos no tienen cuerpo y no es posible definir descriptores de acceso ni un constructor.
La firma de un constructor de TypeScript se puede definir en una interfaz, pero implementarla en una clase se vuelve imposible. Esta particularidad proviene del hecho de que la palabra clave class es un azúcar sintáctico y que el constructor hace referencia a la función constructora.
Sintaxis:
interface InterfaceName {
propertyName: type;
methodName(param1: type, param2: type, ...): type;
}
Ejemplo:
interface Person {
firstName: string;
lastName: string;
getInformation(): string;
}
Las interfaces no necesitan especificar el alcance de los miembros; deben ser publics. Además, las propiedades definidas por una interfaz no se pueden inicializar de forma predeterminada en ella. La inicialización de propiedades siempre se realiza en la implementación de la interfaz y, por tanto, en una clase.
Una clase puede implementar una o más interfaces usando la palabra...
Polimorfismo
El polimorfismo es el tercer pilar de la programación orientada a objetos. Es un concepto que permite tratar de la misma manera objetos de distintos tipos (generalmente, hablamos de una única interfaz o método que puede procesar varios tipos).
Hay varias implementaciones posibles del polimorfismo. La primera consiste en definir varias sobrecargas de un método para cada tipo que debe manejar (consulte la sección Métodos).
El segundo enfoque posible es confiar en las capacidades de abstracción del lenguaje. Es una solución extensible y fácil de mantener con posterioridad, por lo que generalmente se prefiere a la primera solución. En TypeScript, los métodos funcionarán o aceptarán abstracciones como parámetros. Por tanto, es necesario utilizar clases o interfaces abstractas para implementar el polimorfismo en una clase.
Ejemplo:
interface Payable {
sendPayment(): void;
}
class Supplier implements Payable {
constructor(
public readonly name: string,
private readonly invoice: number
) {}
sendPayment(): void {
console.log(
`Pay ${ ...
Los principios SOLID
1. Introducción a los principios SOLID
Los principios SOLID fueron introducidos por Robert C. Martin. Corresponden a un conjunto de buenas prácticas en torno a la programación orientada a objetos.
Bajo el acrónimo SOLID se encuentran los siguientes cinco principios:
-
Single responsibility (Responsabilidad única)
-
Open/Closed (Abierto a la extensión, cerrado a la modificación)
-
Liskov substitution (Sustitución de Liskov)
-
Interface segregation (Segregación de interfaces)
-
Dependency inversion (Inversión de dependencias)
La implementación de estos principios puede mejorar significativamente la capacidad de mantenimiento y escalabilidad del código de una aplicación. Por lo tanto, se recomienda encarecidamente implementarlos.
2. El principio de responsabilidad única
El principio de responsabilidad única establece que una clase tiene una única responsabilidad en un programa. La multiplicación de responsabilidades dentro de una clase complica su código, su lectura, su mantenibilidad, su escalabilidad, lo que aumenta significativamente la probabilidad de generar un error.
No aplicar este principio a menudo conduce a la creación de los God Object dentro del programa. Decimos que un objeto se convierte en un God Object cuando acumula demasiadas responsabilidades. La presencia de un God Object en un programa se considera una mala práctica.
Ejemplo (incumplimiento del principio):
class Employee {
constructor(
readonly firstName: string,
readonly lastName: string,
readonly teams: [string, Employee[]][]
) {}
getTeams() {
return this.teams;
}
createTeam(teamName: string) {
if (!this.teams.some(t => t[0] === teamName)) {
this.teams.push([teamName, []]);
}
}
addMemberToTeam(teamName: string, employee: Employee) {
const filteredTeam = this.teams.filter(t =>...