Ir al contenido
wannacode

Clases abstractas sin abstract

En resumen: JavaScript no tiene abstract: una clase abstracta es una clase padre que detiene new con new.target y lanza un error desde los métodos que la clase hija tiene que escribir.

Problema

Cero metros cuadrados

Estás escribiendo un programa que pide baldosas para una terraza. La terraza tiene tres partes: un rectángulo de 4 × 3 m, una parte redonda con un radio de 1 m y un triángulo con una base de 2 m y una altura de 3 m. Hay una clase padre Shape con area(), que devuelve 0, y cada figura lo sobrescribe.

El colega que escribió Triangle se olvidó:

class Triangle extends Shape {
#base;
#height;
constructor(base, height) {
super();
this.#base = base;
this.#height = height;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Baldosas: ${total.toFixed(2)} m²`);
Salida
Baldosas: 15.14 m²

Necesitas 18,14 m². El programa no da ningún error, y al pedido le faltan 3 m². El cero de la clase padre ha escondido el método que falta.

¿Y qué es new Shape()? Una figura sin forma. Un objeto así no tiene ninguna razón para existir.

La idea

Clase abstracta: una promesa que cumplen las clases hijas

Shape no es una figura real. Lo único que dice es «toda figura tiene un área». Así que hazla abstracta:

class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape es abstracta: crea una figura concreta");
}
}
area() {
throw new Error("area() debe escribirse en la clase hija");
}
}

JavaScript no tiene abstract. La clase padre hace cumplir la regla ella misma, con dos comprobaciones:

  • El método «abstracto» es un método de la clase padre que no devuelve nada razonable y en su lugar lanza un error: throw new Error(...) detiene el programa y dice por qué. Aprenderás más sobre throw en el módulo «Errores y excepciones».
  • La clase «abstracta» comprueba new.target en su constructor: la clase cuyo objeto se está creando. Con new Shape() es Shape, y el constructor lanza un error. Con new Circle(1) es Circle, y la comprobación lo deja pasar.
  • Por eso las clases hijas llaman a super() en su constructor, aunque no tengan nada que pasar.

Nada de esto se comprueba por adelantado. El Triangle olvidado se crea sin quejas. Pero cuando el bucle llega a su area(), el programa se detiene con «Error: area() debe escribirse en la clase hija», en lugar de quedarse corto en 3 m² en silencio.

Un programa que se detiene con un error claro es mejor que un error silencioso en el pedido.
Más precisamente, para curiosos
  • new.target solo funciona en un constructor. Fuera de él es undefined.
  • TypeScript, que es JavaScript con tipos, tiene un abstract de verdad y lo comprueba antes de que arranque el programa. El JavaScript normal no tiene nada parecido.

Míralo

El triángulo cumple la promesa

Añade el método a Triangle:

class Triangle extends Shape {
// ... #base, #height y el constructor
area() {
return this.#base * this.#height / 2;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Baldosas: ${total.toFixed(2)} m²`);
Salida
Baldosas: 18.14 m²

El bucle es el mismo. El array guarda figuras de tres clases, y s.area() llama al método de cada una. El polimorfismo de la lección anterior también funciona con una clase padre abstracta.

Pruébalo

Encuentra el error

El programa se detiene en una de las líneas. ¿En cuál?

Elige la línea con el error.

La idea

Abstracta, pero no vacía

Una clase abstracta puede tener todo lo que tiene una normal: campos, un constructor, métodos normales. Haz que cada figura tenga un nombre y un método que la describa:

class Shape {
#name;
constructor(name) {
if (new.target === Shape) {
throw new Error("Shape es abstracta: crea una figura concreta");
}
this.#name = name;
}
area() {
throw new Error("area() debe escribirse en la clase hija");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} m²`;
}
}

describe() se escribe una vez, en Shape. Llama a area(), que en Shape solo lanza un error. No es un problema: describe() siempre se llama sobre una figura real, y una figura real tiene un área.

De Shape no creas objetos, pero su constructor es útil: lo llaman las clases hijas.

class Circle extends Shape {
#radius;
constructor(radius) {
super("círculo");
this.#radius = radius;
}
// ... area() como antes
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
for (const s of terrace) {
console.log(s.describe());
}
Salida
rectángulo: 12.00 m²
círculo: 3.14 m²
triángulo: 3.00 m²

La clase padre escribe lo común (el nombre, la descripción), y las clases hijas escriben lo distinto (el área).

area() {
throw new Error("area() debe escribirse en la clase hija");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} m²`;
}
const s = new Circle(1);
console.log(s.describe());
Paso a paso, en palabras
  1. Shape «promete» area(): el cuerpo solo lanza un error. No puede haber un objeto que sea solo un Shape.
  2. El objeto es un círculo: la parte Circle tiene su propio area(). Cumple la promesa.
  3. s.describe() está escrito en Shape y sirve para toda figura.
  4. Llama a area(). El objeto es un círculo, así que se ejecuta el método de Circle: π · 1 · 1.
  5. describe() arma el texto. Lo común está en la clase padre, lo distinto en la clase hija.
  6. La salida es el área del círculo, con dos cifras después del punto decimal.

Pruébalo

¿De quién es el método?

¿Qué imprimirá el programa?

// Shape y Circle son de la tarjeta de arriba
const s = new Circle(2);
console.log(s.describe());
Respuesta

Pruébalo

¿Qué falta?

¿Qué tiene que ir en el hueco para que no puedas crear un objeto de Shape?

class Shape {
  constructor() {
    if ( === Shape) {
      throw new Error("Shape es abstracta: crea una figura concreta");
    }
  }

  area() {
    throw new Error("area() debe escribirse en la clase hija");
  }
}

Ejercicio

Figuras y animales

Resuelve las tareas en tu editor.

1. Área y perímetro. Una clase abstracta Shape con un nombre y dos métodos abstractos: area() y perimeter(). Clases hijas Circle (radio) y Rectangle (ancho y alto). Pon un círculo con un radio de 5 y un rectángulo de 4 × 2,5 en un array e imprime los dos valores con dos cifras después del punto decimal.

Salida
Círculo: área 78.54, perímetro 31.42
Rectángulo: área 10.00, perímetro 13.00

2. Animales. Una clase abstracta Animal con un método abstracto sound() y un método normal breathe() que imprime «El animal respira.». Clases hijas Cat y Dog. Llama al sonido de los dos en un bucle, y después a breathe() del primero.

Salida
¡Miau, miau!
¡Guau, guau!
El animal respira.

Ejercicio

Vehículos y actuaciones

3. Vehículos. Una clase abstracta Vehicle con un método abstracto move(). SimpleCar y Bicycle heredan de ella. Bicycle tiene además su propio método pedal().

Salida
El coche arranca
La bicicleta arranca
Pedaleo
Prueba garage[1].pedal(): funciona, porque ahí hay una bicicleta. ¿Y garage[0].pedal()? ¿Por qué el programa se detiene solo en esa línea y no antes?

4. Actuaciones. Una clase abstracta Performance con un título y un método abstracto start(). DanceMove y Song heredan de ella. Song tiene además un método sing().

Salida
Empieza el baile moonwalk
Empieza la canción 'Bohemian Rhapsody'
Canto 'Bohemian Rhapsody'

Más tareas

  • Triángulo. Añade a la tarea 1 un Triangle con sus tres lados. El perímetro es su suma, y el área sale de la fórmula de Herón: s = perímetro / 2, área = √(s(s − a)(s − b)(s − c)). Para los lados 3, 4 y 5 el área es 6.00 y el perímetro es 12.00.
  • Empleado abstracto. Haz abstracta la clase Employee de la lección anterior, con un monthlyPay() abstracto. Añade FullTime (el salario se paga completo) y conserva Manager e Intern. ¿Qué gana el programa? ¿Qué pierde?

Test

Ponte a prueba

1. El constructor de Shape lanza un error si new.target === Shape. ¿Cuál funciona?
2. ¿Tiene JavaScript la palabra clave abstract?
3. Triangle extiende Shape, pero no escribe area(). ¿Qué pasa?
4. ¿Puede una clase «abstracta» tener un constructor, campos y métodos normales?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·