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 = bonusdef 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 = -10staff[0].salary += staff[0].salary * percent // 100for e in staff: print(f"Lohnzettel: {e.name} {monthly_pay(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
elifim 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 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 // 100ivan = Employee("Ivan", 2000)ivan.raise_salary(-10)print(ivan.salary)ivan.raise_salary(10)print(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.
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 = -2000print(ivan.salary)-2000In 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__salaryum. Von außen gibt esivan.__salarynicht 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)'Employee' object has no attribute '__salary'2000Das 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?
class Employee:
def __init__(self, name, salary):
self._name = name
self. = salary_salary: Der Unterstrich sagt „intern für die Klasse, von außen nicht anfassen“. Python verbietet es nicht, aber jeder, der den Code liest, sieht es.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._bonusmaria = Manager("Maria", 3000, 500)maria.raise_salary(-10)maria.raise_salary(10)print(f"{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().__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 // 2Lohnzettel 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)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())Die Erhöhung kommt aus Employee: 1600 + 10% = 1760. amount_due() kommt auch aus Employee, ruft aber monthly_pay() 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:
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._supplierpayments = [ Employee("Ivan", 2000), Manager("Maria", 3000, 500), Intern("Ana", 1600), 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.
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)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} €")
percent = -10staff[0].salary += staff[0].salary * percent // 100class 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 // 100class Manager(Employee): def __init__(self, name, salary, bonus): super().__init__(name, salary) self._bonus = bonus
@property def bonus(self): return self._bonusclass 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 // 2class 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} €")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
- 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_salary() 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 monthly_pay() selbst. Die Überweisung ruft dieselbe Methode auf wie der Lohnzettel, und Ana bekommt in beiden 800 €.
- Abstraktion: Die Überweisung kennt nur den Vertrag 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.
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 = 10staff[0].salary += staff[0].salary * percent // 100for e in staff: print(f"Lohnzettel: {e.name} {monthly_pay(e)} €")transfer(staff)Lohnzettel: Ivan 2200 €Lohnzettel: Maria 3500 €Lohnzettel: Ana 800 €Überweisung: Ivan 2200 €Überweisung: Maria 3500 €Überweisung: Ana 800 €Gesamt: 6500 €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)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)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._supplierProbier 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.
class Manager(Employee):return self.salary + self._bonussuper().__init__(name, salary)def __init__(self, name, salary, bonus):def monthly_pay(self):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:
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.
amount_due() gibt das Gehalt direkt zurück, statt self.monthly_pay() 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 self.monthly_pay().
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 salarybekommt 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)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. 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 -= discount2.
def area(s): if isinstance(s, Circle): return 3.14 * s.r * s.r if isinstance(s, Rectangle): return s.w * s.h return 03.
class Invoice(Employee): # bekommt name und payee() fertig pass4.
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
- 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. 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.
Ü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 0Schreib 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
monthly_pay()einzahlt, nicht das Gehalt. Der Manager muss auch den Bonus bekommen, ohne dass dumonthly_pay()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 als abstrakte Klassen · Typprüfungen
- Aufgaben zum Thema: Vererbung und Polymorphie