Zum Inhalt
wannacode

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);
Ausgabe
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:

  1. 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.
  2. 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.
  3. Eine neue Art, viele Korrekturen. Morgen kommt ein freier Mitarbeiter dazu. Du brauchst ein neues else if im Lohnzettel, in der Überweisung, bei den Steuern, beim Urlaub, überall, wo jemand nach der Rolle fragt. Und jeder Mitarbeiter trägt schon ein Feld bonus, 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());
Ausgabe
2000
2200

-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;
Fehler beim Kompilieren
error: salary has private access in Employee

Das 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
  • private schützt das Feld vor Code außerhalb der Klasse, nicht vor der Klasse selbst: Jede Methode von Employee kann 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 final sein. Dann kann es nicht einmal die Klasse selbst später anfassen.

Probier es aus

Was versteckt das Gehalt?

Das Gehalt soll nur in der Klasse sichtbar sein. Was steht in der Lücke?
class Employee {
    private String name;
     int salary;

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());
Ausgabe
Maria: 3300 + 500

Der 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);
Ausgabe
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());
Antwort

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);
Ausgabe
Ü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);
Schritt für Schritt, in Worten
  1. 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.
  2. Kapselung: Das Gehalt ist versteckt, und raise() lehnt -10% ab. Ivan bleibt bei 2000 €.
  3. 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.
  4. Polymorphie: Jede Klasse berechnet ihr monthlyPay() selbst. Die Überweisung ruft dieselbe Methode auf wie der Lohnzettel, und Ana bekommt in beiden 800 €.
  5. Abstraktion: Die Überweisung kennt nur das Interface Payable. Die Stromrechnung kommt dazu, ohne ein Mitarbeiter zu sein.
  6. 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.

Vorher
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);
Ausgabe
Lohnzettel: Ivan 2200 €
Lohnzettel: Maria 3500 €
Lohnzettel: Ana 800 €
Überweisung: Ivan 2200 €
Überweisung: Maria 3500 €
Überweisung: Ana 800 €
Gesamt: 6500 €
Nachher
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);
Ausgabe
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);
Ausgabe
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.

  1. class Manager extends Employee {
  2. public Manager(String name, int salary, int bonus) {
  3. }
  4. @Override
  5. public int monthlyPay() {
  6. private int bonus;
  7. this.bonus = bonus;
  8. }
  9. }
  10. return getSalary() + bonus;
  11. 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:

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

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());
Ausgabe
2100

Deshalb 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
  1. 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.
  2. 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.
  3. 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.
  4. 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.

Ausgabe
Ü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.

Ausgabe
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 du monthlyPay() in Manager anfasst.
  • 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

1. Das Gehalt ist versteckt und ändert sich nur über raise(), das einen Prozentsatz unter null ablehnt. Welches Prinzip ist das?
2. Lohnzettel und Überweisung berechnen das Gehalt mit zwei Kopien des if nach der Rolle, und in einer davon fehlt die Praktikantin. Welches Prinzip entfernt die zweite Kopie?
3. Worin unterscheiden sich Abstraktion und Kapselung?
4. Eine Rechnung braucht einen Namen und einen Betrag. Ein Kollege schlägt vor, dass Invoice von Employee erbt, um beides fertig zu bekommen. Warum nicht?
5. Welches der beiden ist die Polymorphie, bei der das Objekt entscheidet, während das Programm läuft?

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