OOP-Prinzipien
Kurz gesagt: Objektorientierte Programmierung steht auf vier Prinzipien: Kapselung, Vererbung, Polymorphie und Abstraktion. Sie schützen die Daten, schreiben das Gemeinsame einmal und ersetzen die if-Ketten nach der Art des Objekts.
Problem
Gehälter ohne Prinzipien
Am Monatsende zahlt die Firma die Gehälter. Das Programm wurde in Eile geschrieben: eine Klasse mit öffentlichen Feldern, die Rolle ist Text, und das Gehalt wird an zwei Stellen berechnet. Die Funktion monthlyPay erstellt den Lohnzettel, und transfer überweist das Geld an die Bank.
class Employee { constructor(name, role, salary, bonus) { this.name = name; this.role = role; this.salary = salary; this.bonus = bonus; }}function monthlyPay(e) { if (e.role === "Manager") { return e.salary + e.bonus; } else if (e.role === "Praktikant") { return Math.trunc(e.salary / 2); } return e.salary;}
function transfer(staff) { let total = 0; for (const e of staff) { let amount = e.salary; if (e.role === "Manager") { amount += e.bonus; } console.log(`Überweisung: ${e.name} ${amount} €`); total += amount; } console.log(`Gesamt: ${total} €`);}Heute trägt die Buchhaltung eine Gehaltserhöhung für Ivan ein. Im Formular steht aber -10 statt 10:
const staff = [ new Employee("Ivan", "Mitarbeiter", 2000, 0), new Employee("Maria", "Manager", 3000, 500), new Employee("Ana", "Praktikant", 1600, 0),];const percent = -10;staff[0].salary += Math.trunc(staff[0].salary * percent / 100);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${monthlyPay(e)} €`);}transfer(staff);Lohnzettel: Ivan 1800 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 1800 €Überweisung: Maria 3500 €Überweisung: Ana 1600 €Gesamt: 6900 €Das Programm hält nicht an und beschwert sich nicht. Aber in der Ausgabe stecken drei Risse:
- Ein falscher Schreibzugriff. Das Gehalt von Ivan ist auf 1800 € gefallen. Das Feld ist für jede Zeile im Programm offen, und niemand fragt, ob -10% eine Erhöhung ist.
- Zwei Kopien einer Regel. Anas Lohnzettel sagt 800 €, und die Bank überweist ihr 1600 €. Die Regel „Praktikanten bekommen die Hälfte“ steht im Lohnzettel, in der Überweisung wurde sie vergessen. Die Kopie ist vom Original abgewichen, und niemand hat es bemerkt.
- Eine neue Art, viele Korrekturen. Morgen kommt ein freier Mitarbeiter dazu. Du brauchst ein neues
else ifim Lohnzettel, in der Überweisung, bei den Steuern, beim Urlaub, überall, wo jemand nach der Rolle fragt. Und jeder Mitarbeiter trägt schon ein Feldbonus, obwohl nur der Manager einen Bonus hat.
Objektorientierte Programmierung ist die Antwort auf genau diese drei Dinge.
Die Idee
Die vier Prinzipien
Objektorientierte Programmierung (OOP) ordnet ein Programm um Objekte: Daten zusammen mit den Methoden, die sie anfassen. Sie steht auf vier Prinzipien. Mit jedem davon hast du in den Modulen „Klassen und Objekte“ und „Vererbung und Polymorphie“ schon gearbeitet:
| Prinzip | Was es sagt | Welchen Riss es repariert |
|---|---|---|
| Kapselung | Das Objekt schützt seine Daten selbst. Von außen kommst du nur über Methoden an sie, die prüfen. | Den falschen Schreibzugriff: -10% wird abgelehnt. |
| Vererbung | Was mehrere Klassen gemeinsam haben, steht einmal, in einer Basisklasse. | Der Bonus steht nur in Manager, und das Gemeinsame (Name, Gehalt, die Prüfung bei der Erhöhung) steht einmal in Employee. |
| Polymorphie | Ein Aufruf, und jedes Objekt antwortet auf seine Art. | Das if nach der Rolle: Jede Klasse berechnet ihr Gehalt selbst, ohne zweite Kopie. |
| Abstraktion | Der Code hängt davon ab, was ein Objekt kann, nicht davon, wie es das macht. | Eine neue Art von Zahlung kommt dazu, ohne dass du die Überweisung anfasst. |
Kurz gesagt: Kapselung schützt, Vererbung teilt, Polymorphie wählt, Abstraktion verspricht.
Ab hier repariert jedes Prinzip einen Riss im Gehaltsprogramm. Am Ende siehst du sie zusammen, im selben Programm.
Die Idee
Kapselung: Das Objekt schützt seine Daten
Das Gehalt gehört dem Mitarbeiter. Nur er soll es ändern. Das Feld wird privat, mit #, und die Erhöhung ist eine Methode, die prüft:
class Employee { #name; #salary;
constructor(name, salary) { this.#name = name; this.#salary = salary; }
get name() { return this.#name; }
get salary() { return this.#salary; }
raise(percent) { if (percent > 0) { this.#salary += Math.trunc(this.#salary * percent / 100); } }}const ivan = new Employee("Ivan", 2000);ivan.raise(-10);console.log(ivan.salary);ivan.raise(10);console.log(ivan.salary);20002200-10% wird abgelehnt, 10% geht durch. Die Prüfung steht an einer Stelle, in der Klasse, und jeder Code, der das Gehalt ändern will, muss durch sie hindurch. Das ist Kapselung: Die Daten und die Regeln dafür stehen zusammen im Objekt, und von außen sieht man nur, was die Klasse zeigen will.
Mit # ist das Feld wirklich privat. Ein Programm, in dem außerhalb der Klasse die Zeile
ivan.#salary = -2000;steht, startet gar nicht erst:
SyntaxError: Private field '#salary' must be declared in an enclosing classVon außen wird das Gehalt über den Getter salary gelesen, der keinen Setter hat, und nur mit raise() geändert.
Wann es schadet: Getter und Setter für alles. Wenn jedes Feld einen Getter und einen Setter hat, der alles speichert, was du ihm gibst, ist das Feld immer noch offen, nur mit mehr Zeilen. Kapselung ist nicht „verstecktes Feld plus get und set“. Sie bedeutet: Das Objekt entscheidet, was sich ändern darf und wie. Deshalb hat Employee keine Methode, die das Gehalt schreibt, sondern eine Erhöhung mit Prüfung. Fang mit allem versteckt an und öffne nur, was nötig ist.
Genauer, für Neugierige
#schützt auch vor Unterklassen:Managersieht#salaryebenfalls nicht. Deshalb liest er das Gehalt über den Gettersalary.- Bevor es
#gab (bis 2022), benutzte JavaScript die Abmachung_salary, wie Python. Du triffst sie in älterem Code.
Probier es aus
Was versteckt das Gehalt?
class Employee {
#name;
;#salary: Mit # ist das Feld nur für den Code in Employee sichtbar. Ein Programm, das es außerhalb der Klasse erwähnt, startet nicht einmal.Die Idee
Vererbung: das Gemeinsame einmal
Ein Manager ist ein Mitarbeiter mit Bonus. Statt einer Kopie von Employee mit einem Feld mehr erbt Manager alles und fügt nur den Bonus hinzu:
class Manager extends Employee { #bonus;
constructor(name, salary, bonus) { super(name, salary); this.#bonus = bonus; }
get bonus() { return this.#bonus; }}const maria = new Manager("Maria", 3000, 500);maria.raise(-10);maria.raise(10);console.log(`${maria.name}: ${maria.salary} + ${maria.bonus}`);Maria: 3300 + 500Der Manager hat keine eigene Prüfung bei der Erhöhung. Er nimmt die aus Employee: -10% wird abgelehnt, und 10% hebt 3000 auf 3300. Wenn sich morgen die Regel für Erhöhungen ändert, änderst du sie an einer Stelle, und sie gilt für alle Unterklassen. Das ist Vererbung: Das Gemeinsame steht in der Basisklasse, und die Unterklasse schreibt nur ihren eigenen Teil.
super(name, salary) gibt Name und Gehalt an den Konstruktor der Basisklasse weiter. #salary ist auch vor der Unterklasse versteckt, deshalb liest Manager das Gehalt über den Getter salary. Nach extends steht genau eine Klasse.
Der Test, ob Vererbung sinnvoll ist, ist der Satz „X ist ein Y“. „Ein Manager ist ein Mitarbeiter“ stimmt.
Wann es schadet: lange Ketten und Vererbung wegen fertigem Code. Employee → Manager → SeniorManager → RegionalSeniorManager: Um zu verstehen, was die unterste Klasse macht, liest du vier Klassen, und eine Änderung in der obersten bewegt alles darunter. Ein anderer häufiger Fehler ist, von einer Klasse nur zu erben, um eine fertige Methode zu bekommen: eine Rechnung, die von Employee erbt, damit sie einen Namen hat. „Eine Rechnung ist ein Mitarbeiter“ stimmt nicht, und sie bekäme ein Gehalt und eine Erhöhung. Was du stattdessen machst, steht unten in der Karte „Was man verwechselt“.
Die Idee
Polymorphie: Jede Klasse antwortet selbst
Jetzt der Riss mit den Kopien. Statt eines if nach der Rolle berechnet jede Klasse ihr Monatsgehalt selbst. Die Basisklasse hat eine allgemeine Version, und die Unterklassen überschreiben sie:
class Employee { // ... #name, #salary, salary und raise() von vorher
monthlyPay() { return this.#salary; }}
class Manager extends Employee { // ... #bonus und der Konstruktor
monthlyPay() { return this.salary + this.#bonus; }}
class Intern extends Employee { monthlyPay() { return Math.trunc(this.salary / 2); }}Lohnzettel und Überweisung rufen beide dieselbe Methode auf:
function transfer(staff) { let total = 0; for (const e of staff) { console.log(`Überweisung: ${e.name} ${e.monthlyPay()} €`); total += e.monthlyPay(); } console.log(`Gesamt: ${total} €`);}const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${e.monthlyPay()} €`);}transfer(staff);Lohnzettel: Ivan 2000 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2000 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Gesamt: 6300 €Anna bekommt 800 €, auf dem Lohnzettel und in der Überweisung. Die Regel für Praktikanten steht nur in Intern, und es gibt keine zweite Kopie, die abweichen kann. Eine neue Art von Mitarbeiter ist eine neue Klasse mit ihrer eigenen Methode, und Schleife und Überweisung bleiben unberührt. Das ist Polymorphie: ein Aufruf, e.monthlyPay(), und jedes Objekt antwortet auf seine Art.
JavaScript sucht die Methode nach ihrem Namen: zuerst in der Klasse des Objekts, dann in ihrer Basisklasse. Vorher wird nichts geprüft. Jedes Objekt, das eine Methode mit diesem Namen hat, passt in die Schleife, egal welche Klasse es hat. Das nennt man Duck Typing: „Wenn es läuft wie eine Ente und quakt wie eine Ente, dann ist es eine Ente“.
Wann es schadet. Zwei Fälle, die nie drei werden, brauchen keine Klassenhierarchie: Ein if ist kürzer und klarer. Außerdem: Eine Unterklasse, die die Methode überschreibt, aber das Versprechen der Basisklasse bricht (ein Gehalt, das negativ herauskommt, oder eine Methode, die das Programm anhält), macht jede Schleife kaputt, die der Basisklasse vertraut.
Probier es aus
Wie viel überweist die Bank?
Die Klassen sind aus den Karten oben, zusammen mit dem Zahlungsvertrag aus der nächsten Karte: amountDue() steht in Employee und gibt this.monthlyPay() zurück.
const e = new Intern("Ana", 1600);e.raise(10);console.log(e.amountDue());Die Erhöhung kommt aus Employee: 1600 + 10% = 1760. amountDue() kommt auch aus Employee, ruft aber monthlyPay() auf dem Objekt auf, und das Objekt ist eine Praktikantin. Es läuft die Version aus Intern: 1760 / 2 = 880. Drei Prinzipien in drei Zeilen.
Die Idee
Abstraktion: was, nicht wie
Die Buchhaltung will die Rechnungen der Lieferanten in derselben Überweisung haben. Eine Rechnung ist kein Mitarbeiter. Aber der Überweisung ist es egal, was für ein Ding jedes ist. Sie braucht zwei Dinge: an wen und wie viel. Das ist der Vertrag:
// Der Vertrag „kann bezahlt werden“. Jedes bezahlbare Objekt hat:// amountDue(): wie viel geschuldet wird// payee(): an wen
class Employee { // ... alles von vorher
amountDue() { return this.monthlyPay(); }
payee() { return this.#name; }}
function transfer(payments) { let total = 0; for (const p of payments) { console.log(`Überweisung: ${p.payee()} ${p.amountDue()} €`); total += p.amountDue(); } console.log(`Gesamt: ${total} €`);}class Invoice { #supplier; #amount;
constructor(supplier, amount) { this.#supplier = supplier; this.#amount = amount; }
amountDue() { return this.#amount; }
payee() { return this.#supplier; }}const payments = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600), new Invoice("Stromanbieter", 340),];transfer(payments);Überweisung: Ivan 2000 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Überweisung: Stromanbieter 340 €Gesamt: 6640 €transfer weiß nichts über Gehälter, Boni und Rechnungen. Es kennt nur den Vertrag. Das ist Abstraktion: Du zeigst, was ein Objekt kann, und versteckst, wie es das macht. Die Überweisung hängt von der Abstraktion ab, deshalb kommt alles, was sie erfüllt, ohne Änderung an der Überweisung hinein.
JavaScript hat weder interface noch abstract. Der Vertrag ist eine Abmachung über die Namen der Methoden, in einem Kommentar notiert. Niemand prüft sie: Ein Objekt ohne amountDue() kommt ohne Probleme ins Array, und das Programm hält erst an, wenn die Schleife bei ihm ankommt, mit TypeError: p.amountDue is not a function. Das ist der Preis von Duck Typing.
Ein Vertrag ist eine Form der Abstraktion. Die andere ist eine Klasse, deren Methoden das Programm anhalten, wenn die Unterklasse sie nicht schreibt: Shape verspricht einen Flächeninhalt, und jede Figur sagt, wie er berechnet wird („Abstrakte Klassen“). Wann du was nimmst, steht in der Lektion „Interfaces“.
Wann es schadet: Abstraktion zu früh. Ein Vertrag mit einer einzigen Implementierung, „für alle Fälle“, ist ein Name mehr, eine Datei mehr und ein Sprung mehr beim Lesen. Abstraktion lohnt sich, wenn es mindestens zwei Implementierungen gibt oder wenn du die eine austauschen musst, zum Beispiel in einem Test. Payable kam mit der Rechnung, nicht vorher.
Sieh es dir an
Die vier zusammen
Die Szene geht das Programm von Anfang an durch: Jedes Prinzip repariert seinen Riss, und die Zahlen unten kommen aus demselben Programm.
const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];staff[0].raise(-10);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${e.monthlyPay()} €`);}const payments = [staff[0], staff[1], staff[2], new Invoice("Stromanbieter", 340)];transfer(payments);function monthlyPay(e) { if (e.role === "Manager") { return e.salary + e.bonus; } else if (e.role === "Praktikant") { return Math.trunc(e.salary / 2); } return e.salary;}
function transfer(staff) { let total = 0; for (const e of staff) { let amount = e.salary; if (e.role === "Manager") { amount += e.bonus; } console.log(`Überweisung: ${e.name} ${amount} €`); total += amount; } console.log(`Gesamt: ${total} €`);}
const percent = -10;staff[0].salary += Math.trunc(staff[0].salary * percent / 100);class Employee { #name; #salary;
constructor(name, salary) { this.#name = name; this.#salary = salary; }
get name() { return this.#name; }
get salary() { return this.#salary; }
raise(percent) { if (percent > 0) { this.#salary += Math.trunc(this.#salary * percent / 100); } }}class Manager extends Employee { #bonus;
constructor(name, salary, bonus) { super(name, salary); this.#bonus = bonus; }
get bonus() { return this.#bonus; }}class Employee { // ... #name, #salary, salary und raise() von vorher
monthlyPay() { return this.#salary; }}
class Manager extends Employee { // ... #bonus und der Konstruktor
monthlyPay() { return this.salary + this.#bonus; }}
class Intern extends Employee { monthlyPay() { return Math.trunc(this.salary / 2); }}// Der Vertrag „kann bezahlt werden“. Jedes bezahlbare Objekt hat:// amountDue(): wie viel geschuldet wird// payee(): an wen
class Employee { // ... alles von vorher
amountDue() { return this.monthlyPay(); }
payee() { return this.#name; }}
function transfer(payments) { let total = 0; for (const p of payments) { console.log(`Überweisung: ${p.payee()} ${p.amountDue()} €`); total += p.amountDue(); } console.log(`Gesamt: ${total} €`);}const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];staff[0].raise(-10);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${e.monthlyPay()} €`);}const payments = [staff[0], staff[1], staff[2], new Invoice("Stromanbieter", 340)];transfer(payments);Schritt für Schritt, in Worten
- Vorher: Die Rolle ist Text, die Regel steht kopiert in zwei if-Ketten, und jeder schreibt das Gehalt. Ivan steht bei 1800 €, und Lohnzettel und Überweisung von Ana passen nicht zusammen.
- Kapselung: Das Gehalt ist versteckt, und raise() lehnt -10% ab. Ivan bleibt bei 2000 €.
- Vererbung: Manager nimmt Name, Gehalt und die Prüfung bei der Erhöhung aus Employee und fügt nur den Bonus hinzu. Die Objekte haben jetzt Klassen statt einer Rolle als Text.
- Polymorphie: Jede Klasse berechnet ihr monthlyPay() selbst. Die Überweisung ruft dieselbe Methode auf wie der Lohnzettel, und Ana bekommt in beiden 800 €.
- Abstraktion: Die Überweisung kennt nur den Vertrag: amountDue() und payee(). Die Stromrechnung kommt dazu, ohne ein Mitarbeiter zu sein.
- Alle vier zusammen: Dieselben -10% werden abgelehnt, jeder Lohnzettel stimmt mit seiner Überweisung überein, und insgesamt sind es 6640 €.
Hier ist das Programm vorher und nachher, mit 10% für Ivan. Die Prozedur „vorher“ wurde von Hand repariert: Die Regel für Praktikanten steht jetzt auch in der Überweisung.
function monthlyPay(e) { if (e.role === "Manager") { return e.salary + e.bonus; } else if (e.role === "Praktikant") { return Math.trunc(e.salary / 2); } return e.salary;}
function transfer(staff) { let total = 0; for (const e of staff) { let amount = e.salary; if (e.role === "Manager") { amount += e.bonus; } else if (e.role === "Praktikant") { amount = Math.trunc(e.salary / 2); } console.log(`Überweisung: ${e.name} ${amount} €`); total += amount; } console.log(`Gesamt: ${total} €`);}const staff = [ new Employee("Ivan", "Mitarbeiter", 2000, 0), new Employee("Maria", "Manager", 3000, 500), new Employee("Ana", "Praktikant", 1600, 0),];const percent = 10;staff[0].salary += Math.trunc(staff[0].salary * percent / 100);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${monthlyPay(e)} €`);}transfer(staff);Lohnzettel: Ivan 2200 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2200 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Gesamt: 6500 €function transfer(payments) { let total = 0; for (const p of payments) { console.log(`Überweisung: ${p.payee()} ${p.amountDue()} €`); total += p.amountDue(); } console.log(`Gesamt: ${total} €`);}const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];staff[0].raise(10);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${e.monthlyPay()} €`);}transfer(staff);Lohnzettel: Ivan 2200 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2200 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Gesamt: 6500 €Die Ausgabe ist dieselbe. Der Unterschied liegt darin, was morgen passiert:
- Jemand gibt -10 ein →
raise()lehnt es ab (Kapselung). - Eine neue Regel für Erhöhungen → sie ändert sich an einer Stelle, in
Employee(Vererbung). - Eine neue Art von Mitarbeiter → eine neue Klasse mit ihrem eigenen
monthlyPay(); Lohnzettel und Überweisung bleiben unberührt (Polymorphie). - Etwas Neues zu bezahlen → eine neue Klasse, die den Vertrag erfüllt; die Überweisung bleibt unberührt (Abstraktion).
Dasselbe Programm mit den falschen -10% aus dem Problem und mit der Rechnung:
const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];staff[0].raise(-10);for (const e of staff) { console.log(`Lohnzettel: ${e.name} ${e.monthlyPay()} €`);}const payments = [staff[0], staff[1], staff[2], new Invoice("Stromanbieter", 340)];transfer(payments);Lohnzettel: Ivan 2000 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2000 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Überweisung: Stromanbieter 340 €Gesamt: 6640 €Alle Klassen an einer Stelle
// Der Vertrag „kann bezahlt werden“. Jedes bezahlbare Objekt hat:// amountDue(): wie viel geschuldet wird// payee(): an wen
class Employee { #name; #salary;
constructor(name, salary) { this.#name = name; this.#salary = salary; }
get name() { return this.#name; }
get salary() { return this.#salary; }
raise(percent) { if (percent > 0) { this.#salary += Math.trunc(this.#salary * percent / 100); } }
monthlyPay() { return this.#salary; }
amountDue() { return this.monthlyPay(); }
payee() { return this.#name; }}
class Manager extends Employee { #bonus;
constructor(name, salary, bonus) { super(name, salary); this.#bonus = bonus; }
get bonus() { return this.#bonus; }
monthlyPay() { return this.salary + this.#bonus; }}
class Intern extends Employee { monthlyPay() { return Math.trunc(this.salary / 2); }}
class Invoice { #supplier; #amount;
constructor(supplier, amount) { this.#supplier = supplier; this.#amount = amount; }
amountDue() { return this.#amount; }
payee() { return this.#supplier; }}Probier es aus
Bring den Manager in Ordnung
Die Zeilen von Manager sind durcheinander. Bring sie in die übliche Reihenfolge: das Feld, der Konstruktor, dann die Methode für das Gehalt.
super(name, salary);class Manager extends Employee {this.#bonus = bonus;constructor(name, salary, bonus) {}#bonus;}}monthlyPay() {return this.salary + this.#bonus;
Probier es aus
Finde den Fehler
Ein Kollege hat den Vertrag in Employee „vereinfacht“. Das Programm ist das aus „Die vier zusammen“, und es gibt aus:
Lohnzettel: Ivan 2200 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2200 €Überweisung: Maria 3000 €Überweisung: Ana 1600 €Gesamt: 6800 €Marias Lohnzettel sagt 3500 €, und die Bank überweist ihr 3000 €. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
amountDue() gibt das Gehalt direkt zurück, statt this.monthlyPay() zu fragen. Das ist wieder die zweite Kopie der Regel aus dem Problem, nur kürzer: Der Bonus des Managers und die Hälfte der Praktikantin fehlen. Eine Methode der Basisklasse, die eine überschriebene Methode aufruft, ist genau die Stelle, an der Polymorphie für dich arbeitet. Die Korrektur: return this.monthlyPay();.
Die Idee
Was man verwechselt
Abstraktion oder Kapselung? Beide verstecken etwas, aber Verschiedenes. Kapselung versteckt die Daten eines Objekts, damit niemand von außen sie kaputt macht: Das Gehalt ist versteckt, die Erhöhung prüft. Abstraktion versteckt die Unterschiede zwischen vielen Klassen hinter einem gemeinsamen Versprechen: Die Überweisung sieht nur amountDue() und payee(). Kapselung sieht das Objekt von innen, Abstraktion sieht den Code, der es benutzt, von außen.
Vererbung oder Komposition? Vererbung sagt „ist ein“: Ein Manager ist ein Mitarbeiter. Komposition sagt „hat ein“: Ein Mitarbeiter hat ein Bankkonto. Wenn du den Mitarbeiter zur Unterklasse des Kontos aus der Lektion „Kapselung“ machst, um deposit() fertig zu bekommen,
class Employee extends Account { // ... name und salary}bekommt er auch Abhebungen, einen Kontostand und alles, was das Konto morgen dazulernt. Und „Ein Mitarbeiter ist ein Konto“ stimmt nicht. Stattdessen ist das Konto ein Feld des Mitarbeiters:
class Employee { #name; #salary; #account;
constructor(name, salary, account) { this.#name = name; this.#salary = salary; this.#account = account; }
payday() { this.#account.deposit(this.#salary); }}const account = new Account(100);const ivan = new Employee("Ivan", 2000, account);ivan.payday();console.log(account.balance);2100Deshalb lautet der häufige Rat „Komposition vor Vererbung“. Erbe nur, wenn „X ist ein Y“ stimmt und stimmen wird. Sonst soll das Objekt das andere als Feld haben.
Überladen oder Überschreiben? In Java und C# wird beides manchmal Polymorphie genannt. Überladen sind mehrere Methoden mit einem Namen und verschiedenen Parametern. JavaScript hat das nicht: Eine zweite Funktion mit demselben Namen ersetzt die erste, und stattdessen gibt es Standardwerte (die Lektion dazu). Polymorphie in der OOP ist das Überschreiben: Die Wahl trifft das Objekt, während das Programm läuft.
Genauer, für Neugierige
- Manche Bücher zählen drei Säulen auf: Kapselung, Vererbung und Polymorphie. Abstraktion ist dann die gemeinsame Idee darunter. Im Vorstellungsgespräch werden meistens vier erwartet.
- Die Prinzipien sind keine Regeln, die man „zu 100%“ befolgt. Jedes hat einen Preis: Versteckte Felder bedeuten mehr Methoden, Hierarchien bedeuten mehr Klassen, Verträge bedeuten einen Namen mehr. Du setzt sie dort ein, wo der Riss echt ist.
- Nach diesen vier kommen die fünf SOLID-Prinzipien für Code, der Änderungen übersteht, und die Entwurfsmuster: fertige Lösungen, gebaut aus denselben vier. Sie stehen im Kurs „Softwaretechnik“: SOLID und Entwurfsmuster.
Übung
Welches Prinzip ist verletzt?
Eine klassische Frage im Vorstellungsgespräch. Beantworte für jedes Stück laut: Welches Prinzip ist verletzt, was wird kaputtgehen und wie reparierst du es?
1.
class Cart { total = 0;}
// ... an 12 Stellen im Programm:cart.total -= discount;2.
function area(s) { if (s instanceof Circle) return 3.14 * s.r * s.r; if (s instanceof Rectangle) return s.w * s.h; return 0;}3.
class Invoice extends Employee { // bekommt name und payee() fertig}4.
// die einzige Implementierung:class DefaultSalaryProvider { provide(e) { return e.monthlyPay(); }}
const salaries = new DefaultSalaryProvider();console.log(salaries.provide(maria));Die Antworten
- Kapselung. Die Summe ist offen und wird an 12 Stellen verringert. Ein Rabatt, der größer ist als die Summe, macht sie negativ, und niemand fragt. Die Summe wird #total, und der Rabatt ist eine Methode des Warenkorbs, die prüft.
- Polymorphie (und Abstraktion). Ein if nach der Art der Figur: Eine neue Figur bedeutet eine neue Zeile hier und überall, wo jemand nach der Art fragt. Der Flächeninhalt wird eine abstrakte Methode von
Shape, und jede Figur schreibt ihre eigene. - Vererbung an der falschen Stelle. „Eine Rechnung ist ein Mitarbeiter“ stimmt nicht: Sie bekommt ein Gehalt, eine Erhöhung und eine monatliche Zahlung. Gemeinsam ist ihnen, dass sie bezahlt werden: ein Vertrag
Payable. - Abstraktion zu früh. Eine Klasse, die nur weiterleitet an
monthlyPay(), ist ein Name mehr und ein Sprung mehr beim Lesen und schützt vor nichts. Entferne sie und ruf die Methode direkt auf. Der Vertrag wird sinnvoll, wenn eine zweite Implementierung auftaucht.
Übung
Ein Honorar und eine Terrasse
Löse die Aufgaben in deinem Editor.
1. Ein freier Mitarbeiter. Contractor (Peter, 40 Stunden zu 25 €) ist kein Mitarbeiter: Er hat keinen Lohnzettel, muss aber in die Überweisung. Füge ihn so hinzu, dass sich transfer um keine einzige Zeile ändert. Nimm auch eine Mietrechnung in die Überweisung auf, 1800 € für den Vermieter.
Überweisung: Ivan 2000 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Überweisung: Peter 1000 €Überweisung: Vermieter 1800 €Gesamt: 9100 €2. Die Terrasse ohne if. Diese Prozedur berechnet den Flächeninhalt nach der Art der Figur:
function area(kind, a, b) { if (kind === "rechteck") return a * b; if (kind === "dreieck") return a * b / 2; if (kind === "quadrat") return a * a; return 0;}Schreib sie mit einer abstrakten Shape und den Klassen Rectangle, Triangle und Square. Die Terrasse ist ein Rechteck 4 × 3, ein Dreieck mit Grundseite 2 und Höhe 3 und ein Quadrat mit Seitenlänge 2. Ein Quadrat ist ein Rechteck mit gleichen Seiten: Schreib es als Unterklasse von Rectangle, ohne eigene Formel für den Flächeninhalt.
Rechteck: 12 m²Dreieck: 3 m²Quadrat: 4 m²Gesamt: 19 m²Weitere Aufgaben
- Ein Lohnzettel auch für den freien Mitarbeiter. Wenn der freie Mitarbeiter doch einen Lohnzettel will, ohne Mitarbeiter zu werden: Was muss im Vertrag stehen, und wer erfüllt ihn?
- Gehalt aufs Konto. Gib dem Mitarbeiter ein Konto (Komposition, wie in „Was man verwechselt“) und eine Methode, die am Zahltag
monthlyPay()einzahlt, nicht das Gehalt. Der Manager muss auch den Bonus bekommen, ohne dass dumonthlyPay()inManageranfasst. - Eine Erhöhung für alle. Erhöhe das Gehalt aller Mitarbeiter in einer Schleife um 5%. Welche Klassen fasst du an?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Polymorphie · Abstrakte Klassen ohne abstract · Interfaces ohne interface · Typprüfungen
- Aufgaben zum Thema: Vererbung und Polymorphie