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 wenclass 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()} €`);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} €`);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
- Das Array enthält drei Objekte aus drei Klassen. Gemeinsam haben sie nur die Methode amountDue(), und die Pfeile erreichen nur sie.
- total beginnt bei 0.
- p zeigt auf den Mitarbeiter: amountDue() gibt das Gehalt zurück, 2000.
- Der Manager ist ein Mitarbeiter, also kann auch er bezahlt werden. Gehalt plus Bonus: 3500.
- Die Rechnung ist kein Mitarbeiter, aber sie hält denselben Vertrag ein: 340.
- 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());Manager erbt amountDue() von Employee, also hält auch der Manager den Vertrag ein, ohne es zu schreiben. Der Aufruf geht an die überschriebene Methode des Managers: 3000 + 500.
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());}Stromanbieter;340Vermieter;1800extends 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
payee() funktioniert, aber power.amountDue() hält das Programm an. In welcher Zeile ist der Fehler?Wähle die Zeile mit dem Fehler.
amountdue mit kleinem d. Für JavaScript ist das eine andere Methode, und amountDue gibt es nicht. Niemand prüft den Vertrag, also zeigt sich der Fehler erst beim Aufruf: TypeError: power.amountDue is not a function.Probier es aus
Was fehlt?
class Invoice {
// ...
() {
return this.#amount;
}
}amountDue, genau so, mit großem D. In JavaScript verbindet nichts anderes die Klasse mit dem Vertrag als der Name der Methode.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“.
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.assignin 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();}Der Vogel schlägt mit den FlügelnGleitet durch die LuftDas Flugzeug hebt abGleitet 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.
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.
Name: Georg, Alter: 45Name: Elena, Alter: 19Notendurchschnitt: 5.53. Tanz. Ein Interface Danceable mit einer Methode dance(). DanceMove (ein Name und eine Dauer) implementiert es.
Ich tanze Salsa für 30 Sekunden4. 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.
Kein Lied ausgewähltIch spiele 'Bohemian Rhapsody'5. Der Teich. Interfaces Swimmable mit swim() und Flyable mit fly(). Fish schwimmt. Duck schwimmt und fliegt.
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 inSubscription(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
Employeeden CSV-Vertrag implementieren, mit einer Zeile „Name;Monatsgehalt“. Leg Mitarbeiter und Rechnungen in ein Array.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Kapselung · Vererbung · Polymorphie · Abstrakte Klassen ohne abstract
- Geht weiter in: Typprüfungen · OOP-Prinzipien · Werfen und eigene Exceptions · Dateien
- Aufgaben zum Thema: Vererbung und Polymorphie