Ir al contenido
wannacode

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én
Una clase que cumple el contrato simplemente escribe los métodos con esos nombres. No se lo dice a nadie:
class 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()} €`);
Salida
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} €`);
Salida
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
  1. El array guarda tres objetos de tres clases. Lo único que comparten es el método amountDue(), y las flechas solo llegan a él.
  2. total empieza en 0.
  3. p apunta al empleado: amountDue() devuelve el salario, 2000.
  4. El gerente es un empleado, así que también se le puede pagar. Salario más bono: 3500.
  5. La factura no es un empleado, pero cumple el mismo contrato: 340.
  6. 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());
Respuesta

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());
}
Salida
Compañía eléctrica;340
Casero;1800

extends 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

La factura se crea y payee() funciona, pero power.amountDue() detiene el programa. ¿Qué línea tiene el error?

Elige la línea con el error.

Pruébalo

¿Qué falta?

class Invoice {
  // ...

  () {
    return this.#amount;
  }
}

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».
A menudo se usan juntos: 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();
}
Salida
El pájaro bate las alas
Planea por el aire
El avión despega
Planea 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.

Salida
¡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.

Salida
Nombre: Jorge, edad: 45
Nombre: Elena, edad: 19
Nota media: 5.5

3. Baile. Una interfaz Danceable con un método dance(). DanceMove (un nombre y una duración) la implementa.

Salida
Bailo salsa durante 30 segundos

4. 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.

Salida
No hay ninguna canción elegida
Reproduzco 'Bohemian Rhapsody'

5. El estanque. Interfaces Swimmable con swim() y Flyable con fly(). Fish nada. Duck nada y vuela.

Salida
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 en Subscription (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 Employee también implemente el contrato CSV, con una línea «nombre;salario mensual». Pon empleados y facturas en un solo array.

Test

Ponte a prueba

1. En JavaScript, ¿cómo dice una clase que cumple el contrato «se puede pagar»?
2. Un objeto sin amountDue() acaba en el array de pagos. ¿Cuándo te enteras?
3. La clase escribe amountdue() en lugar de amountDue(). ¿Qué pasa?
4. ¿Cuántos contratos puede cumplir una clase?
5. Hay que pagar a los empleados y las facturas en un solo bucle. No comparten código. ¿Qué encaja mejor?

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