Interfaces sin interface
En resumen: JavaScript no tiene interface: el contrato es una lista de métodos, y un objeto lo cumple simplemente si tiene métodos con esos nombres (duck typing).
Problema
Salarios y facturas
A final de mes la empresa paga dos tipos de cosas: los salarios de los empleados y las facturas de los proveedores. Quieres un solo array y un solo bucle: a quién se paga, cuánto, y el total al final.
Una factura no es un empleado. Hacer que herede de Employee está mal. La frase «una factura es un empleado» no es cierta, y además la factura tendría un nombre, un salario y un aumento.
Una clase padre común Payment con campos y código tampoco es la respuesta: los empleados y las facturas no tienen datos comunes ni código común. Lo único que tienen en común es que se pueden pagar.
La idea
Una interfaz: un contrato «puede hacer»
Una interfaz es una lista de métodos: qué tiene que saber hacer una clase, pero no cómo. JavaScript no tiene interface, y tampoco tiene abstract. El contrato es solo un acuerdo sobre los nombres de los métodos, escrito en un comentario:
// El contrato «se puede pagar». Todo objeto pagable tiene:// amountDue(): cuánto se debe// payee(): a quiénclass Invoice { #supplier; #amount;
constructor(supplier, amount) { this.#supplier = supplier; this.#amount = amount; }
amountDue() { return this.#amount; }
payee() { return this.#supplier; }}
const power = new Invoice("Compañía eléctrica", 340);console.log(`${power.payee()}: ${power.amountDue()} €`);Compañía eléctrica: 340 €El lenguaje no sabe nada del contrato. No hay implements: es una palabra reservada, y un programa con class Invoice implements Payable no arranca (SyntaxError: Unexpected strict mode reserved word). Así que los nombres cumplen el contrato: un objeto que tiene amountDue() y payee() se puede pagar. Esto se llama duck typing: «si camina como un pato y grazna como un pato, es un pato».
Míralo
Un solo bucle para todo lo que se paga
El empleado también acepta el contrato. Su importe es el salario mensual:
class Employee { // ... #name, #salary, el constructor, monthlyPay()
amountDue() { return this.monthlyPay(); }
payee() { return this.#name; }}Ahora tanto los empleados como las facturas tienen amountDue() y payee(). El array no tiene tipo, y nada los une salvo que los nombres de los métodos coinciden. Para el bucle es suficiente.
const payments = [ new Employee("Juan", 2000), new Manager("María", 3000, 500), new Invoice("Compañía eléctrica", 340), new Invoice("Casero", 1800),];let total = 0;for (const p of payments) { console.log(`${p.payee()}: ${p.amountDue()} €`); total += p.amountDue();}console.log(`Total: ${total} €`);Juan: 2000 €María: 3500 €Compañía eléctrica: 340 €Casero: 1800 €Total: 7640 €El gerente no escribe nada nuevo. De Employee hereda tanto el contrato como amountDue(). Y amountDue() llama a monthlyPay(), que el gerente ha sobrescrito. Por eso María recibe 3500 €.
¿Y si en el array acaba un objeto que no cumple el contrato? Aquí la entrada tiene payee(), pero no amountDue():
class Ticket { payee() { return "Cine"; }}
const payments = [new Invoice("Compañía eléctrica", 340), new Ticket()];for (const p of payments) { console.log(`${p.payee()}: ${p.amountDue()} €`);}El array la acepta sin preguntar. La factura se paga, y con la entrada el programa se detiene: TypeError: p.amountDue is not a function. Ves el error solo cuando el bucle llega a la entrada, no cuando la entrada entra en el array.
const payments = [ new Employee("Juan", 2000), new Manager("María", 3000, 500), new Invoice("Compañía eléctrica", 340),];let total = 0;for (const p of payments) { total += p.amountDue();}console.log(`Total: ${total} €`);Paso a paso, en palabras
- El array guarda tres objetos de tres clases. Lo único que comparten es el método amountDue(), y las flechas solo llegan a él.
- total empieza en 0.
- p apunta al empleado: amountDue() devuelve el salario, 2000.
- El gerente es un empleado, así que también se le puede pagar. Salario más bono: 3500.
- La factura no es un empleado, pero cumple el mismo contrato: 340.
- El bucle no conoce las clases. Solo sabe que a cada uno se le puede pagar.
Pruébalo
¿Cuál es el importe?
¿Qué imprimirá el programa? Las clases son de la tarjeta de arriba.
const p = new Manager("María", 3000, 500);console.log(p.amountDue());Manager hereda amountDue() de Employee, así que el gerente también cumple el contrato, sin escribirlo. La llamada va al método sobrescrito del gerente: 3000 + 500.
La idea
Una clase padre, muchos contratos
Contabilidad también quiere las facturas en formato CSV: una línea «proveedor;importe» por cada una. Ese es un segundo contrato. En JavaScript no hay nada que enumerar: la factura simplemente escribe también el método de ese contrato.
// El segundo contrato: toCsv() devuelve una línea «proveedor;importe».
class Invoice { // ... como antes
toCsv() { return `${this.#supplier};${this.#amount}`; }}const forAccounting = [new Invoice("Compañía eléctrica", 340), new Invoice("Casero", 1800)];for (const e of forAccounting) { console.log(e.toCsv());}Compañía eléctrica;340Casero;1800extends acepta una sola clase padre, pero los contratos no son clases padre. El gerente sigue siendo extends Employee y solo añade toCsv(). Así, una clase cuya clase padre ya está ocupada puede entrar igualmente en un array común con clases completamente distintas.
Pruébalo
Encuentra el error
payee() funciona, pero power.amountDue() detiene el programa. ¿Qué línea tiene el error?Elige la línea con el error.
amountdue con una d minúscula. Para JavaScript es otro método, y amountDue no existe. Nadie comprueba el contrato, así que el error aparece solo en la llamada: TypeError: power.amountDue is not a function.Pruébalo
¿Qué falta?
class Invoice {
// ...
() {
return this.#amount;
}
}amountDue, exactamente así, con D mayúscula. En JavaScript nada más une la clase al contrato, salvo el nombre del método.La idea
¿Clase abstracta o interfaz?
En JavaScript ninguna de las dos forma parte del lenguaje. Una clase padre abstracta es una clase que se detiene con throw. Un contrato son solo los nombres de los métodos. La diferencia está en lo que dicen:
| Clase abstracta | Contrato | |
|---|---|---|
| Dice | «X es un Y» | «X puede» |
| Cómo se escribe | extends una clase padre que lanza un error |
nada: solo métodos con esos nombres |
| Cuántos por clase | uno | todos los que quieras |
| Campos y constructor | sí | no |
| Código común | sí | no |
- Una clase abstracta, cuando las clases de debajo son un mismo tipo de cosa y comparten código y datos: las figuras tienen un nombre y una descripción, los empleados tienen un nombre y un salario.
- Un contrato, cuando las clases son distintas pero necesitan una capacidad común: un empleado y una factura «se pueden pagar», un pájaro y un avión «pueden volar».
Employee es la clase padre del gerente y del becario, y a la vez cumple el contrato de pago.Más precisamente, para curiosos
- En Java y C# una interfaz puede tener un método por defecto, con cuerpo. En JavaScript el contrato no existe como nada en el código, así que no hay dónde poner un método así. El comportamiento común lo pones en un objeto y lo copias a las clases con
Object.assign. Cuidado: la copia reemplaza un método con el mismo nombre si la clase ya tiene uno propio.
const gliding = { glide() { console.log("Planea por el aire"); },};
class Bird { fly() { console.log("El pájaro bate las alas"); }}
class Plane { fly() { console.log("El avión despega"); }}
Object.assign(Bird.prototype, gliding);Object.assign(Plane.prototype, gliding);
const flyers = [new Bird(), new Plane()];for (const f of flyers) { f.fly(); f.glide();}El pájaro bate las alasPlanea por el aireEl avión despegaPlanea por el aire- Si quieres comprobar el contrato antes de llamar al método, preguntas si el objeto tiene una función así:
typeof p.amountDue === "function". Hay más en la lección «Comprobaciones de tipo».
Ejercicio
Contratos
Resuelve las tareas en tu editor. En JavaScript la «interfaz» de cada tarea es un contrato en un comentario: las clases simplemente tienen los métodos con esos nombres.
1. Claxon. Una interfaz Honkable con un método honk(). SimpleCar del módulo «Clases y objetos» la implementa.
¡Bip, bip!2. Datos. Una interfaz Printable con un método printDetails(). Impleméntala en Person, y haz que Student (una clase hija de Person) la sobrescriba y añada la nota. Pon los dos en un solo array.
Nombre: Jorge, edad: 45Nombre: Elena, edad: 19Nota media: 5.53. Baile. Una interfaz Danceable con un método dance(). DanceMove (un nombre y una duración) la implementa.
Bailo salsa durante 30 segundos4. DJ. Una clase Song con un título. Una interfaz Playable con un método play(). DJ la implementa y tiene un método playSong(song) que recuerda la canción y la reproduce. Si todavía no hay canción, play() lo dice.
No hay ninguna canción elegidaReproduzco 'Bohemian Rhapsody'5. El estanque. Interfaces Swimmable con swim() y Flyable con fly(). Fish nada. Duck nada y vuela.
El pez nada.El pato nada.El pato vuela.Más tareas
- Descuento. Una interfaz Discountable con un método que devuelve el precio después del descuento. Impleméntala en
Product(10 % de descuento) y enSubscription(25 % de descuento para estudiantes). Calcula el total de un carrito con un producto de 50 € y una suscripción de estudiante de 12 €: 45 € + 9 € = 54 €. - CSV para todos. Haz que
Employeetambién implemente el contrato CSV, con una línea «nombre;salario mensual». Pon empleados y facturas en un solo array.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · Herencia · Polimorfismo · Clases abstractas sin abstract
- Continúa en: Comprobaciones de tipo · Principios de la POO · Lanzar excepciones y crear las tuyas · Archivos
- Ejercicios sobre el tema: Herencia y polimorfismo