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²`);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 sobrethrowen el módulo «Errores y excepciones». - La clase «abstracta» comprueba
new.targeten su constructor: la clase cuyo objeto se está creando. Connew Shape()esShape, y el constructor lanza un error. Connew Circle(1)esCircle, 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.
Más precisamente, para curiosos
new.targetsolo funciona en un constructor. Fuera de él esundefined.- TypeScript, que es JavaScript con tipos, tiene un
abstractde 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²`);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
Elige la línea con el error.
new Shape() es un intento de crear un objeto de una clase abstracta. La primera línea está bien: new Array(3) crea un array de tres casillas vacías para figuras, no figuras. En ellas puedes poner un rectángulo, un círculo o un triángulo. El constructor de Shape ve que new.target es Shape y detiene el programa con «Shape es abstracta: crea una figura concreta».
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());}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
- Shape «promete» area(): el cuerpo solo lanza un error. No puede haber un objeto que sea solo un Shape.
- El objeto es un círculo: la parte Circle tiene su propio area(). Cumple la promesa.
- s.describe() está escrito en Shape y sirve para toda figura.
- Llama a area(). El objeto es un círculo, así que se ejecuta el método de Circle: π · 1 · 1.
- describe() arma el texto. Lo común está en la clase padre, lo distinto en la clase hija.
- 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());El objeto es un círculo con un radio de 2. describe() pertenece a Shape, pero el area() de dentro es el del círculo: π · 2 · 2 ≈ 12,57.
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");
}
}new.target: la clase cuyo objeto se está creando. Solo con new Shape() es Shape, así que las clases hijas pasan.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.
Círculo: área 78.54, perímetro 31.42Rectángulo: área 10.00, perímetro 13.002. 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.
¡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().
El coche arrancaLa bicicleta arrancaPedaleogarage[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().
Empieza el baile moonwalkEmpieza la canción 'Bohemian Rhapsody'Canto 'Bohemian Rhapsody'Más tareas
- Triángulo. Añade a la tarea 1 un
Trianglecon 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
Employeede la lección anterior, con unmonthlyPay()abstracto. AñadeFullTime(el salario se paga completo) y conservaManagereIntern. ¿Qué gana el programa? ¿Qué pierde?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Métodos de instancia · Encapsulamiento · Herencia · Polimorfismo
- Continúa en: Interfaces sin interface · Comprobaciones de tipo · Principios de la POO · Lanzar excepciones y crear las tuyas
- Ejercicios sobre el tema: Herencia y polimorfismo