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 Methode monthlyPay erstellt den Lohnzettel, und transfer überweist das Geld an die Bank.
class Employee { public String name; public String role; public int salary; public int bonus;
public Employee(String name, String role, int salary, int bonus) { this.name = name; this.role = role; this.salary = salary; this.bonus = bonus; }}static int monthlyPay(Employee e) { if (e.role.equals("Manager")) { return e.salary + e.bonus; } else if (e.role.equals("Praktikant")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("Manager")) { amount += e.bonus; } System.out.println("Überweisung: " + e.name + " " + amount + " €"); total += amount; } System.out.println("Gesamt: " + total + " €");}Heute trägt die Buchhaltung eine Gehaltserhöhung für Ivan ein. Im Formular steht aber -10 statt 10:
Employee[] staff = { new Employee("Ivan", "Mitarbeiter", 2000, 0), new Employee("Maria", "Manager", 3000, 500), new Employee("Ana", "Praktikant", 1600, 0)};int percent = -10;staff[0].salary += staff[0].salary * percent / 100;for (Employee e : staff) { System.out.println("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 private, und die Erhöhung ist eine Methode, die prüft:
class Employee { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }}Employee ivan = new Employee("Ivan", 2000);ivan.raise(-10);System.out.println(ivan.getSalary());ivan.raise(10);System.out.println(ivan.getSalary());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.
Eine Zeile, die direkt ins Gehalt schreibt, lässt sich nicht mehr kompilieren:
ivan.salary = -2000;error: salary has private access in EmployeeDas Gehalt wird mit dem Getter getSalary() gelesen und nur mit raise() geändert. In Java schreibst du Getter von Hand.
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
privateschützt das Feld vor Code außerhalb der Klasse, nicht vor der Klasse selbst: Jede Methode vonEmployeekann es anfassen. Deshalb halten die Methoden der Klasse die Regel ein.- Ein Feld, das sich nach dem Konstruktor nicht mehr ändert (der Name), kann auch
finalsein. Dann kann es nicht einmal die Klasse selbst später anfassen.
Probier es aus
Was versteckt das Gehalt?
class Employee {
private String name;
int salary;private: Das Feld ist nur für den Code in Employee sichtbar. Von außen ändert sich das Gehalt nur mit raise().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 { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }}Manager maria = new Manager("Maria", 3000, 500);maria.raise(-10);maria.raise(10);System.out.println(maria.getName() + ": " + maria.getSalary() + " + " + maria.getBonus());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.
Das Gehalt ist private in Employee, deshalb sieht auch Manager es nicht direkt. Er gibt Name und Gehalt mit super(name, salary) an den Konstruktor der Basisklasse weiter und liest sie danach wie jeder andere, über getSalary(). In Java und C# hat eine Klasse genau eine Basisklasse.
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, getSalary() und raise() von vorher
public int monthlyPay() { return salary; }}
class Manager extends Employee { // ... bonus und der Konstruktor
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { // ... der Konstruktor
@Override public int monthlyPay() { return getSalary() / 2; }}Lohnzettel und Überweisung rufen beide dieselbe Methode auf:
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { System.out.println("Überweisung: " + e.getName() + " " + e.monthlyPay() + " €"); total += e.monthlyPay(); } System.out.println("Gesamt: " + total + " €");}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};for (Employee e : staff) { System.out.println("Lohnzettel: " + e.getName() + " " + 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.
In Java kann eine Methode standardmäßig überschrieben werden, ohne besonderes Wort in der Basisklasse (außer sie ist final oder private). Welche Version läuft, entscheidet das Objekt, während das Programm läuft, nicht der Typ der Variable. @Override lässt den Compiler prüfen, dass du wirklich eine Methode der Basisklasse überschreibst und nicht eine neue mit falsch geschriebenem Namen anlegst.
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 monthlyPay() zurück.
Employee e = new Intern("Ana", 1600);e.raise(10);System.out.println(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:
interface Payable { int amountDue();
String payee();}
class Employee implements Payable { // ... alles von vorher
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Überweisung: " + p.payee() + " " + p.amountDue() + " €"); total += p.amountDue(); } System.out.println("Gesamt: " + total + " €");}class Invoice implements Payable { private String supplier; private int amount;
public Invoice(String supplier, int amount) { this.supplier = supplier; this.amount = amount; }
@Override public int amountDue() { return amount; }
@Override public String payee() { return supplier; }}Payable[] 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.
In Java ist der Vertrag ein interface. Employee und Invoice haben keine gemeinsame Basisklasse außer Object, aber beide schreiben implements Payable, und der Compiler prüft, dass die Methoden geschrieben sind. Manager und Intern bekommen den Vertrag von Employee.
Ein Vertrag ist eine Form der Abstraktion. Die andere ist die abstrakte Klasse: 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 Interface 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.
Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Lohnzettel: " + e.getName() + " " + e.monthlyPay() + " €");}Payable[] payments = { staff[0], staff[1], staff[2], new Invoice("Stromanbieter", 340) };transfer(payments);static int monthlyPay(Employee e) { if (e.role.equals("Manager")) { return e.salary + e.bonus; } else if (e.role.equals("Praktikant")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("Manager")) { amount += e.bonus; } System.out.println("Überweisung: " + e.name + " " + amount + " €"); total += amount; } System.out.println("Gesamt: " + total + " €");}
int percent = -10;staff[0].salary += staff[0].salary * percent / 100;class Employee { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }}class Manager extends Employee { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }}class Employee { // ... name, salary, getSalary() und raise() von vorher
public int monthlyPay() { return salary; }}
class Manager extends Employee { // ... bonus und der Konstruktor
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { // ... der Konstruktor
@Override public int monthlyPay() { return getSalary() / 2; }}interface Payable { int amountDue();
String payee();}
class Employee implements Payable { // ... alles von vorher
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Überweisung: " + p.payee() + " " + p.amountDue() + " €"); total += p.amountDue(); } System.out.println("Gesamt: " + total + " €");}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Lohnzettel: " + e.getName() + " " + e.monthlyPay() + " €");}Payable[] 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 das Interface Payable. 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.
static int monthlyPay(Employee e) { if (e.role.equals("Manager")) { return e.salary + e.bonus; } else if (e.role.equals("Praktikant")) { return e.salary / 2; } return e.salary;}
static void transfer(Employee[] staff) { int total = 0; for (Employee e : staff) { int amount = e.salary; if (e.role.equals("Manager")) { amount += e.bonus; } else if (e.role.equals("Praktikant")) { amount = e.salary / 2; } System.out.println("Überweisung: " + e.name + " " + amount + " €"); total += amount; } System.out.println("Gesamt: " + total + " €");}Employee[] staff = { new Employee("Ivan", "Mitarbeiter", 2000, 0), new Employee("Maria", "Manager", 3000, 500), new Employee("Ana", "Praktikant", 1600, 0)};int percent = 10;staff[0].salary += staff[0].salary * percent / 100;for (Employee e : staff) { System.out.println("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 €static void transfer(Payable[] payments) { int total = 0; for (Payable p : payments) { System.out.println("Überweisung: " + p.payee() + " " + p.amountDue() + " €"); total += p.amountDue(); } System.out.println("Gesamt: " + total + " €");}Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(10);for (Employee e : staff) { System.out.println("Lohnzettel: " + e.getName() + " " + 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:
Employee[] staff = { new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600)};staff[0].raise(-10);for (Employee e : staff) { System.out.println("Lohnzettel: " + e.getName() + " " + e.monthlyPay() + " €");}Payable[] 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
interface Payable { int amountDue();
String payee();}
class Employee implements Payable { private String name; private int salary;
public Employee(String name, int salary) { this.name = name; this.salary = salary; }
public String getName() { return name; }
public int getSalary() { return salary; }
public void raise(int percent) { if (percent > 0) { salary += salary * percent / 100; } }
public int monthlyPay() { return salary; }
@Override public int amountDue() { return monthlyPay(); }
@Override public String payee() { return name; }}
class Manager extends Employee { private int bonus;
public Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
public int getBonus() { return bonus; }
@Override public int monthlyPay() { return getSalary() + bonus; }}
class Intern extends Employee { public Intern(String name, int salary) { super(name, salary); }
@Override public int monthlyPay() { return getSalary() / 2; }}
class Invoice implements Payable { private String supplier; private int amount;
public Invoice(String supplier, int amount) { this.supplier = supplier; this.amount = amount; }
@Override public int amountDue() { return amount; }
@Override public String payee() { return 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.
class Manager extends Employee {public Manager(String name, int salary, int bonus) {}@Overridepublic int monthlyPay() {private int bonus;this.bonus = bonus;}}return getSalary() + bonus;super(name, salary);
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 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 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 { private String name; private int salary; private Account account;
public Employee(String name, int salary, Account account) { this.name = name; this.salary = salary; this.account = account; }
public void payday() { account.deposit(salary); }}Account account = new Account(100);Employee ivan = new Employee("Ivan", 2000, account);ivan.payday();System.out.println(account.getBalance());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? Beides wird manchmal Polymorphie genannt. Überladen sind mehrere Methoden mit einem Namen und verschiedenen Parametern: Der Compiler wählt nach den Argumenten, bevor das Programm startet. Überschreiben ist dieselbe Methode in einer Unterklasse: Die Wahl trifft das Objekt, während das Programm läuft. Wenn man in der OOP „Polymorphie“ sagt, meint man fast immer das Zweite.
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 { public double total;}
// ... an 12 Stellen im Programm:cart.total -= discount;2.
static double area(Shape s) { if (s instanceof Circle c) return 3.14 * c.r * c.r; if (s instanceof Rectangle r) return r.w * r.h; return 0;}3.
class Invoice extends Employee { // bekommt name und payee() fertig}4.
interface SalaryProvider { int provide(Employee e);}
// die einzige Implementierung:class DefaultSalaryProvider implements SalaryProvider { public int provide(Employee e) { return e.monthlyPay(); }}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 private, 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. Ein Vertrag mit einer Implementierung, 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:
static int area(String kind, int a, int b) { if (kind.equals("rechteck")) return a * b; if (kind.equals("dreieck")) return a * b / 2; if (kind.equals("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 · Interfaces · Typprüfungen
- Aufgaben zum Thema: Vererbung und Polymorphie