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 Funktion monthly_pay erstellt den Lohnzettel, und transfer überweist das Geld an die Bank.

class Employee:
def __init__(self, name, role, salary, bonus):
self.name = name
self.role = role
self.salary = salary
self.bonus = bonus
def monthly_pay(e):
if e.role == "Manager":
return e.salary + e.bonus
elif e.role == "Praktikant":
return e.salary // 2
return e.salary
def transfer(staff):
total = 0
for e in staff:
amount = e.salary
if e.role == "Manager":
amount += e.bonus
print(f"Überweisung: {e.name} {amount} €")
total += amount
print(f"Gesamt: {total} €")

Heute trägt die Buchhaltung eine Gehaltserhöhung für Ivan ein. Im Formular steht aber -10 statt 10:

staff = [
Employee("Ivan", "Mitarbeiter", 2000, 0),
Employee("Maria", "Manager", 3000, 500),
Employee("Ana", "Praktikant", 1600, 0),
]
percent = -10
staff[0].salary += staff[0].salary * percent // 100
for e in staff:
print(f"Lohnzettel: {e.name} {monthly_pay(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 elif 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 intern, mit Unterstrich, und die Erhöhung ist eine Methode, die prüft:

class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
@property
def name(self):
return self._name
@property
def salary(self):
return self._salary
def raise_salary(self, percent):
if percent > 0:
self._salary += self._salary * percent // 100
ivan = Employee("Ivan", 2000)
ivan.raise_salary(-10)
print(ivan.salary)
ivan.raise_salary(10)
print(ivan.salary)
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.

Python hat kein private. Der Unterstrich ist eine Abmachung unter Programmierern: „intern, von außen nicht anfassen“. Die Sprache hält niemanden auf. Eine Zeile, die direkt ins Feld schreibt, läuft:

ivan._salary = -2000
print(ivan.salary)
Ausgabe
-2000

In Python schützen deshalb die Menschen, die den Code lesen, die Kapselung, und Werkzeuge wie PyCharm, das eine solche Zeile unterstreicht. Von außen wird das Gehalt über die Property salary gelesen, die keinen Setter hat, und nur mit raise_salary() 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
  • Mit zwei Unterstrichen (self.__salary) benennt Python das Feld in _Employee__salary um. Von außen gibt es ivan.__salary nicht mehr, aber über den neuen Namen kommst du trotzdem hin:
class Employee:
def __init__(self, name, salary):
self.__salary = salary
ivan = Employee("Ivan", 2000)
try:
print(ivan.__salary)
except AttributeError as error:
print(error)
print(ivan._Employee__salary)
Ausgabe
'Employee' object has no attribute '__salary'
2000

Das schützt vor einem zufälligen Namenskonflikt in Unterklassen, nicht davor, dass jemand das Feld absichtlich anfasst. Meist reicht ein Unterstrich.

Probier es aus

Was versteckt das Gehalt?

Das Gehalt ist intern für die Klasse. Wie heißt das Feld nach der Abmachung (mit einem Unterstrich)?
class Employee:
    def __init__(self, name, salary):
        self._name = name
        self. = 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(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
@property
def bonus(self):
return self._bonus
maria = Manager("Maria", 3000, 500)
maria.raise_salary(-10)
maria.raise_salary(10)
print(f"{maria.name}: {maria.salary} + {maria.bonus}")
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.

super().__init__(name, salary) gibt Name und Gehalt an das __init__ der Basisklasse weiter, und danach liest Manager sie über die Property salary. In Python kann eine Klasse auch mehrere Basisklassen haben (die Lektion „Vererbung“), aber am besten trägt nur eine davon Felder und Code.

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:
# ... __init__, salary und raise_salary() von vorher
def monthly_pay(self):
return self._salary
class Manager(Employee):
# ... __init__ und bonus
def monthly_pay(self):
return self.salary + self._bonus
class Intern(Employee):
def monthly_pay(self):
return self.salary // 2

Lohnzettel und Überweisung rufen beide dieselbe Methode auf:

def transfer(staff):
total = 0
for e in staff:
print(f"Überweisung: {e.name} {e.monthly_pay()} €")
total += e.monthly_pay()
print(f"Gesamt: {total} €")
staff = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Intern("Ana", 1600),
]
for e in staff:
print(f"Lohnzettel: {e.name} {e.monthly_pay()} €")
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.monthly_pay(), und jedes Objekt antwortet auf seine Art.

Python 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: amount_due() steht in Employee und gibt self.monthly_pay() zurück.

e = Intern("Ana", 1600)
e.raise_salary(10)
print(e.amount_due())
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:

class Payable(ABC):
@abstractmethod
def amount_due(self):
pass
@abstractmethod
def payee(self):
pass
class Employee(Payable):
# ... alles von vorher
def amount_due(self):
return self.monthly_pay()
def payee(self):
return self._name
def transfer(payments):
total = 0
for p in payments:
print(f"Überweisung: {p.payee()} {p.amount_due()} €")
total += p.amount_due()
print(f"Gesamt: {total} €")
class Invoice(Payable):
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier
payments = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Intern("Ana", 1600),
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.

Python hat kein interface. Der Vertrag ist eine abstrakte Klasse (ABC), in der jede Methode @abstractmethod hat. Geprüft wird, wenn ein Objekt erzeugt wird: Eine Klasse, die Payable in ihre Klammern schreibt, aber amount_due() nicht geschrieben hat, kann keine Objekte haben. transfer selbst prüft nichts: Jedes Objekt mit diesen zwei Methoden geht durch.

Hier ist der Vertrag eine abstrakte Klasse ohne Code. Eine abstrakte Klasse kann auch gemeinsamen Code tragen: 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.

staff = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Intern("Ana", 1600),
]
staff[0].raise_salary(-10)
for e in staff:
print(f"Lohnzettel: {e.name} {e.monthly_pay()} €")
payments = [staff[0], staff[1], staff[2], 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_salary() 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 monthly_pay() selbst. Die Überweisung ruft dieselbe Methode auf wie der Lohnzettel, und Ana bekommt in beiden 800 €.
  5. Abstraktion: Die Überweisung kennt nur den Vertrag 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
def monthly_pay(e):
if e.role == "Manager":
return e.salary + e.bonus
elif e.role == "Praktikant":
return e.salary // 2
return e.salary
def transfer(staff):
total = 0
for e in staff:
amount = e.salary
if e.role == "Manager":
amount += e.bonus
elif e.role == "Praktikant":
amount = e.salary // 2
print(f"Überweisung: {e.name} {amount} €")
total += amount
print(f"Gesamt: {total} €")
staff = [
Employee("Ivan", "Mitarbeiter", 2000, 0),
Employee("Maria", "Manager", 3000, 500),
Employee("Ana", "Praktikant", 1600, 0),
]
percent = 10
staff[0].salary += staff[0].salary * percent // 100
for e in staff:
print(f"Lohnzettel: {e.name} {monthly_pay(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
def transfer(payments):
total = 0
for p in payments:
print(f"Überweisung: {p.payee()} {p.amount_due()} €")
total += p.amount_due()
print(f"Gesamt: {total} €")
staff = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Intern("Ana", 1600),
]
staff[0].raise_salary(10)
for e in staff:
print(f"Lohnzettel: {e.name} {e.monthly_pay()} €")
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_salary() 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 monthly_pay(); 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:

staff = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Intern("Ana", 1600),
]
staff[0].raise_salary(-10)
for e in staff:
print(f"Lohnzettel: {e.name} {e.monthly_pay()} €")
payments = [staff[0], staff[1], staff[2], 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
from abc import ABC, abstractmethod
class Payable(ABC):
@abstractmethod
def amount_due(self):
pass
@abstractmethod
def payee(self):
pass
class Employee(Payable):
def __init__(self, name, salary):
self._name = name
self._salary = salary
@property
def name(self):
return self._name
@property
def salary(self):
return self._salary
def raise_salary(self, percent):
if percent > 0:
self._salary += self._salary * percent // 100
def monthly_pay(self):
return self._salary
def amount_due(self):
return self.monthly_pay()
def payee(self):
return self._name
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
@property
def bonus(self):
return self._bonus
def monthly_pay(self):
return self.salary + self._bonus
class Intern(Employee):
def monthly_pay(self):
return self.salary // 2
class Invoice(Payable):
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier

Probier es aus

Bring den Manager in Ordnung

Die Zeilen von Manager sind durcheinander. Bring sie in die übliche Reihenfolge: der Konstruktor, der zuerst die Basisklasse aufruft, dann die Methode für das Gehalt.

  1. class Manager(Employee):
  2. return self.salary + self._bonus
  3. super().__init__(name, salary)
  4. def __init__(self, name, salary, bonus):
  5. def monthly_pay(self):
  6. self._bonus = 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:

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 amount_due() 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(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:
def __init__(self, name, salary, account):
self._name = name
self._salary = salary
self._account = account
def payday(self):
self._account.deposit(self._salary)
account = Account(100)
ivan = Employee("Ivan", 2000, account)
ivan.payday()
print(account.balance)
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? In Java und C# wird beides manchmal Polymorphie genannt. Überladen sind mehrere Methoden mit einem Namen und verschiedenen Parametern. Python 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:
def __init__(self):
self.total = 0
# ... an 12 Stellen im Programm:
cart.total -= discount

2.

def area(s):
if isinstance(s, Circle):
return 3.14 * s.r * s.r
if isinstance(s, Rectangle):
return s.w * s.h
return 0

3.

class Invoice(Employee):
# bekommt name und payee() fertig
pass

4.

class SalaryProvider(ABC):
@abstractmethod
def provide(self, e):
pass
# die einzige Implementierung:
class DefaultSalaryProvider(SalaryProvider):
def provide(self, e):
return e.monthly_pay()
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 _total, 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 monthly_pay(), 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:

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

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 monthly_pay() einzahlt, nicht das Gehalt. Der Manager muss auch den Bonus bekommen, ohne dass du monthly_pay() 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_salary(), 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. Was braucht ein Objekt, um durch transfer() zu gehen?

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