Zum Inhalt
wannacode

Interfaces ohne interface

Kurz gesagt: JavaScript hat kein interface: Der Vertrag ist eine Liste von Methoden, und ein Objekt erfüllt ihn einfach, indem es Methoden mit diesen Namen hat (Duck Typing).

Problem

Gehälter und Rechnungen

Am Monatsende bezahlt die Firma zwei Arten von Dingen: die Gehälter der Mitarbeiter und die Rechnungen der Lieferanten. Du willst ein Array und eine Schleife: wer bezahlt wird, wie viel, und am Ende die Summe.

Eine Rechnung ist kein Mitarbeiter. Sie von Employee erben zu lassen ist falsch. Der Satz „eine Rechnung ist ein Mitarbeiter“ stimmt nicht, und die Rechnung bekäme auch einen Namen, ein Gehalt und eine Erhöhung.

Eine gemeinsame Basisklasse Payment mit Feldern und Code ist auch nicht die richtige Antwort: Mitarbeiter und Rechnungen haben keine gemeinsamen Daten und keinen gemeinsamen Code. Gemeinsam ist ihnen nur, dass sie bezahlt werden können.

Die Idee

Ein Interface: ein Vertrag „kann etwas“

Ein Interface ist eine Liste von Methoden: was eine Klasse können muss, aber nicht wie. JavaScript hat kein interface und auch kein abstract. Der Vertrag ist nur eine Abmachung über die Namen der Methoden, in einem Kommentar aufgeschrieben:

// Der Vertrag „kann bezahlt werden“. Jedes zahlbare Objekt hat:
// amountDue(): wie viel geschuldet wird
// payee(): an wen
Eine Klasse, die den Vertrag einhält, schreibt einfach die Methoden mit diesen Namen. Sie sagt niemandem, dass sie es tut:
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("Stromanbieter", 340);
console.log(`${power.payee()}: ${power.amountDue()} €`);
Ausgabe
Stromanbieter: 340 €

Die Sprache weiß nichts von dem Vertrag. Es gibt kein implements: Es ist ein reserviertes Wort, und ein Programm mit class Invoice implements Payable startet nicht (SyntaxError: Unexpected strict mode reserved word). Also halten die Namen den Vertrag ein: Ein Objekt, das amountDue() und payee() hat, kann bezahlt werden. Das nennt man Duck Typing: „Wenn es wie eine Ente läuft und wie eine Ente quakt, ist es eine Ente.“

Sieh es dir an

Eine Schleife für alles, was bezahlt wird

Auch der Mitarbeiter nimmt den Vertrag an. Sein Betrag ist das Monatsgehalt:

class Employee {
// ... #name, #salary, der Konstruktor, monthlyPay()
amountDue() {
return this.monthlyPay();
}
payee() {
return this.#name;
}
}

Jetzt haben sowohl die Mitarbeiter als auch die Rechnungen amountDue() und payee(). Das Array hat keinen Typ, und nichts verbindet sie außer den passenden Namen der Methoden. Das reicht der Schleife.

const payments = [
new Employee("Ivan", 2000),
new Manager("Maria", 3000, 500),
new Invoice("Stromanbieter", 340),
new Invoice("Vermieter", 1800),
];
let total = 0;
for (const p of payments) {
console.log(`${p.payee()}: ${p.amountDue()} €`);
total += p.amountDue();
}
console.log(`Gesamt: ${total} €`);
Ausgabe
Ivan: 2000 €
Maria: 3500 €
Stromanbieter: 340 €
Vermieter: 1800 €
Gesamt: 7640 €

Der Manager schreibt nichts Neues. Von Employee erbt er sowohl den Vertrag als auch amountDue(). Und amountDue() ruft monthlyPay() auf, das der Manager überschrieben hat. Deshalb bekommt Maria 3500 €.

Was, wenn ein Objekt, das den Vertrag nicht einhält, im Array landet? Das Ticket hier hat payee(), aber kein amountDue():

class Ticket {
payee() {
return "Kino";
}
}
const payments = [new Invoice("Stromanbieter", 340), new Ticket()];
for (const p of payments) {
console.log(`${p.payee()}: ${p.amountDue()} €`);
}

Das Array nimmt es ohne Fragen. Die Rechnung wird bezahlt, und beim Ticket bricht das Programm ab: TypeError: p.amountDue is not a function. Den Fehler siehst du erst, wenn die Schleife das Ticket erreicht, nicht wenn das Ticket ins Array kommt.

const payments = [
new Employee("Ivan", 2000),
new Manager("Maria", 3000, 500),
new Invoice("Stromanbieter", 340),
];
let total = 0;
for (const p of payments) {
total += p.amountDue();
}
console.log(`Gesamt: ${total} €`);
Schritt für Schritt, in Worten
  1. Das Array enthält drei Objekte aus drei Klassen. Gemeinsam haben sie nur die Methode amountDue(), und die Pfeile erreichen nur sie.
  2. total beginnt bei 0.
  3. p zeigt auf den Mitarbeiter: amountDue() gibt das Gehalt zurück, 2000.
  4. Der Manager ist ein Mitarbeiter, also kann auch er bezahlt werden. Gehalt plus Bonus: 3500.
  5. Die Rechnung ist kein Mitarbeiter, aber sie hält denselben Vertrag ein: 340.
  6. Die Schleife kennt die Klassen nicht. Sie weiß nur, dass jedes Objekt bezahlt werden kann.

Probier es aus

Wie hoch ist der Betrag?

Was gibt das Programm aus? Die Klassen sind aus der Karte oben.

const p = new Manager("Maria", 3000, 500);
console.log(p.amountDue());
Antwort

Die Idee

Eine Basisklasse, viele Verträge

Die Buchhaltung will die Rechnungen auch im CSV-Format: je eine Zeile „Lieferant;Betrag“. Das ist ein zweiter Vertrag. In JavaScript gibt es nichts aufzuzählen: Die Rechnung schreibt einfach auch die Methode dieses Vertrags.

// Der zweite Vertrag: toCsv() gibt eine Zeile „Lieferant;Betrag“ zurück.
class Invoice {
// ... wie vorher
toCsv() {
return `${this.#supplier};${this.#amount}`;
}
}
const forAccounting = [new Invoice("Stromanbieter", 340), new Invoice("Vermieter", 1800)];
for (const e of forAccounting) {
console.log(e.toCsv());
}
Ausgabe
Stromanbieter;340
Vermieter;1800

extends nimmt nur eine Basisklasse, aber Verträge sind keine Basisklassen. Der Manager bleibt extends Employee und fügt nur toCsv() hinzu. So kann eine Klasse, deren Basisklasse schon vergeben ist, trotzdem mit ganz anderen Klassen in ein gemeinsames Array.

Probier es aus

Finde den Fehler

Die Rechnung wird erzeugt und payee() funktioniert, aber power.amountDue() hält das Programm an. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

class Invoice {
  // ...

  () {
    return this.#amount;
  }
}

Die Idee

Abstrakte Klasse oder Interface?

In JavaScript gehört keins von beiden zur Sprache. Eine abstrakte Basisklasse ist eine Klasse, die mit throw anhält. Ein Vertrag sind nur die Namen der Methoden. Der Unterschied liegt darin, was sie sagen:

Abstrakte Klasse Vertrag
Sagt „X ist ein Y“ „X kann“
Wie du es schreibst extends einer Basisklasse, die einen Fehler wirft nichts: nur Methoden mit diesen Namen
Wie viele pro Klasse eine beliebig viele
Felder und Konstruktor ja nein
Gemeinsamer Code ja nein
  • Eine abstrakte Klasse, wenn die Klassen darunter eine Art von Ding sind und Code und Daten teilen: Figuren haben einen Namen und eine Beschreibung, Mitarbeiter einen Namen und ein Gehalt.
  • Ein Vertrag, wenn die Klassen verschieden sind, aber eine gemeinsame Fähigkeit brauchen: Ein Mitarbeiter und eine Rechnung „können bezahlt werden“, ein Vogel und ein Flugzeug „können fliegen“.
Oft werden sie zusammen benutzt: Employee ist die Basisklasse von Manager und Praktikant und hält gleichzeitig den Vertrag für Zahlungen ein.
Genauer, für Neugierige
  • In Java und C# kann ein Interface eine Standardmethode haben, mit Rumpf. In JavaScript existiert der Vertrag im Code nicht als etwas Greifbares, also gibt es keinen Ort für so eine Methode. Das gemeinsame Verhalten legst du in ein Objekt und kopierst es mit Object.assign in die Klassen. Vorsicht: Die Kopie ersetzt eine Methode mit demselben Namen, wenn die Klasse schon eine eigene hat.
const gliding = {
glide() {
console.log("Gleitet durch die Luft");
},
};
class Bird {
fly() {
console.log("Der Vogel schlägt mit den Flügeln");
}
}
class Plane {
fly() {
console.log("Das Flugzeug hebt ab");
}
}
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();
}
Ausgabe
Der Vogel schlägt mit den Flügeln
Gleitet durch die Luft
Das Flugzeug hebt ab
Gleitet durch die Luft
  • Willst du den Vertrag prüfen, bevor du die Methode aufrufst, fragst du, ob das Objekt so eine Funktion hat: typeof p.amountDue === "function". Mehr dazu in der Lektion „Typprüfungen“.

Übung

Verträge

Löse die Aufgaben in deinem Editor. In JavaScript ist das „Interface“ in jeder Aufgabe ein Vertrag in einem Kommentar: Die Klassen haben einfach die Methoden mit diesen Namen.

1. Hupe. Ein Interface Honkable mit einer Methode honk(). SimpleCar aus dem Modul „Klassen und Objekte“ implementiert es.

Ausgabe
Tut-tut!

2. Angaben. Ein Interface Printable mit einer Methode printDetails(). Implementiere es in Person, und lass Student (eine Unterklasse von Person) es überschreiben und den Notendurchschnitt hinzufügen. Leg beide in ein Array.

Ausgabe
Name: Georg, Alter: 45
Name: Elena, Alter: 19
Notendurchschnitt: 5.5

3. Tanz. Ein Interface Danceable mit einer Methode dance(). DanceMove (ein Name und eine Dauer) implementiert es.

Ausgabe
Ich tanze Salsa für 30 Sekunden

4. DJ. Eine Klasse Song mit einem Titel. Ein Interface Playable mit einer Methode play(). DJ implementiert es und hat eine Methode playSong(song), die sich das Lied merkt und es abspielt. Gibt es noch kein Lied, sagt play() das.

Ausgabe
Kein Lied ausgewählt
Ich spiele 'Bohemian Rhapsody'

5. Der Teich. Interfaces Swimmable mit swim() und Flyable mit fly(). Fish schwimmt. Duck schwimmt und fliegt.

Ausgabe
Der Fisch schwimmt.
Die Ente schwimmt.
Die Ente fliegt.

Weitere Aufgaben

  • Rabatt. Ein Interface Discountable mit einer Methode, die den Preis nach dem Rabatt zurückgibt. Implementiere es in Product (10% Rabatt) und in Subscription (25% Rabatt für Studierende). Berechne die Summe eines Warenkorbs mit einem Produkt für 50 € und einem Studentenabo für 12 €: 45 € + 9 € = 54 €.
  • CSV für alle. Lass auch Employee den CSV-Vertrag implementieren, mit einer Zeile „Name;Monatsgehalt“. Leg Mitarbeiter und Rechnungen in ein Array.

Test

Teste dich

1. Wie sagt eine Klasse in JavaScript, dass sie den Vertrag „kann bezahlt werden“ einhält?
2. Ein Objekt ohne amountDue() landet im Array der Zahlungen. Wann merkst du das?
3. Die Klasse schreibt amountdue() statt amountDue(). Was passiert?
4. Wie viele Verträge kann eine Klasse einhalten?
5. Mitarbeiter und Rechnungen sollen in einer Schleife bezahlt werden. Sie haben keinen gemeinsamen Code. Was passt besser?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·