Die ob­jekt­ori­en­tier­te Pro­gram­mie­rung (OOP) findet überall Anwendung: Zum Schreiben von Be­triebs­sys­te­men, kom­mer­zi­el­le Software und Open Source kommen ob­jekt­ori­en­tier­te Tech­no­lo­gien zum Einsatz. Dabei zeigen sich die Vorteile von OOP erst ab einer gewisser Kom­ple­xi­tät des Projekts. Immer noch ist der ob­jekt­ori­en­tier­te Pro­gram­mier­stil eines der vor­herr­schen­des Pro­gram­mier­pa­ra­dig­men.

Was ist ob­jekt­ori­en­tier­te Pro­gram­mie­rung und wozu wird sie benötigt?

Der Begriff „ob­jekt­ori­en­tier­te Pro­gram­mie­rung“ wurde gegen Ender der 1960er Jahre von Pro­gram­mier-Legende Alan Kay geprägt. Dieser war Mit­ent­wick­ler der bahn­bre­chen­den ob­jekt­ori­en­tier­ten Pro­gram­mier­spra­che Smalltalk, die von Simula, der ersten Sprache mit OOP-Features, be­ein­flusst wurde. Die grund­le­gen­den Ideen von Smalltalk wirken sich bis heute auf die OOP-Features moderner Pro­gram­mier­spra­chen aus. Zu den von Smalltalk be­ein­fluss­ten Sprachen zählen u. a. Ruby, Python, Go und Swift.

Die ob­jekt­ori­en­tier­te Pro­gram­mie­rung zählt neben der populären funk­tio­na­len Pro­gram­mie­rung (FP) zu den vor­herr­schen­den Pro­gram­mier­pa­ra­dig­men. Pro­gram­mier­an­sät­ze lassen sich in die zwei großen Strö­mun­gen „imperativ“ und „de­kla­ra­tiv“ einordnen. Dabei ist OOP eine Aus­prä­gung des im­pe­ra­ti­ven Pro­gram­mier­stils und spe­zi­fisch eine Wei­ter­ent­wick­lung der pro­ze­du­ra­len Pro­gram­mie­rung:

  1. Im­pe­ra­ti­ve Pro­gram­mie­rung: In einzelnen Schritten be­schrei­ben, wie ein Problem zu lösen ist – Beispiel: Al­go­rith­mus
    • Struk­tu­rier­te Pro­gram­mie­rung
      • Pro­ze­du­ra­le Pro­gram­mie­rung
        • Ob­jekt­ori­en­tier­te Pro­gram­mie­rung
  2. De­kla­ra­ti­ve Pro­gram­mie­rung: Er­geb­nis­se nach gewissen Regeln erzeugen – Beispiel: SQL-Abfrage
    • Funk­tio­na­le Pro­gram­mie­rung
    • Do­mä­nen­spe­zi­fi­sche Pro­gram­mie­rung
Hinweis

Die Begriffe „Prozedur“ und „Funktion“ werden oft synonym verwendet. Bei beiden handelt es sich um aus­führ­ba­re Blöcke von Code, die Argumente ent­ge­gen­neh­men können. Der Un­ter­schied liegt darin, dass Funk­tio­nen einen Wert zu­rück­ge­ben, während dies bei Pro­ze­du­ren nicht der Fall ist. Nicht alle Sprachen bieten explizite Un­ter­stüt­zung für Pro­ze­du­ren.

Prin­zi­pi­ell ist es möglich, jegliches Pro­gram­mier­pro­blem mit jedem der Pa­ra­dig­men zu lösen; denn alle Pa­ra­dig­men sind „Turing-komplett“. Das li­mi­tie­ren­de Element ist also nicht die Maschine, sondern der Mensch. Einzelne Pro­gram­mie­ren­de bzw. Pro­gram­mier­teams können nur eine begrenzte Menge an Kom­ple­xi­tät über­bli­cken. Sie nutzen daher Abs­trak­tio­nen, um der Kom­ple­xi­tät Herr zu werden. Je nach Ein­satz­ge­biet und Pro­blem­stel­lung eignet sich der eine oder andere Pro­gram­mier­stil besonders gut.

Die meisten modernen Sprachen sind so­ge­nann­te Multi-Pa­ra­dig­men-Sprachen, die die Pro­gram­mie­rung in mehreren Pro­gram­mier­sti­len erlauben. Dem­ge­gen­über stehen Sprachen, die nur einen einzigen Pro­gram­mier­stil un­ter­stüt­zen; dies trifft vor allem auf strikt funk­tio­na­le Sprachen wie Haskell zu:

Paradigma Merkmale Besonders geeignet für Sprachen
Imperativ OOP Objekte, Klassen, Methoden, Vererbung, Po­ly­mor­phis­mus Mo­del­lie­rung, Sys­tem­de­sign Smalltalk, Java, Ruby, Python, Swift
Imperativ Pro­ze­du­ral Kon­troll­fluss, Iteration, Pro­ze­du­ren / Funk­tio­nen Se­quen­zi­el­le Da­ten­ver­ar­bei­tung C, Pascal, Basic
De­kla­ra­tiv Funk­tio­nal Im­mu­ta­bi­li­ty, Pure Functions, Lambda Calculus, Rekursion, Typ-Systeme Parallele Da­ten­ver­ar­bei­tung, ma­the­ma­ti­sche und wis­sen­schaft­li­che An­wen­dun­gen, Parser und Compiler Lisp, Haskell, Clojure
De­kla­ra­tiv Domain-specific language (DSL) Aus­drucks­stark, großer Sprach­um­fang Do­mä­nen­spe­zi­fi­sche An­wen­dun­gen SQL, CSS
Hinweis

Über­ra­schen­der­wei­se handelt es sich selbst bei CSS um eine Turing-komplette Sprache. Das bedeutet, dass sich jegliche in anderen Sprachen ge­schrie­be­ne Be­rech­nun­gen auch in CSS lösen ließen.

Ob­jekt­ori­en­tier­te Pro­gram­mie­rung ist Teil der im­pe­ra­ti­ven Pro­gram­mie­rung und her­vor­ge­gan­gen aus der pro­ze­du­ra­len Pro­gram­mie­rung. Letztere befasst sich im Grunde mit inerten Daten, die durch aus­führ­ba­ren Code ver­ar­bei­tet werden:

  1. Daten: Werte, Da­ten­struk­tu­ren, Variablen
  2. Code: Ausdrücke, Kon­troll­struk­tu­ren, Funk­tio­nen

Genau hierin liegt der Un­ter­schied zwischen der ob­jekt­ori­en­tier­ten und der pro­ze­du­ra­len Pro­gram­mie­rung: OOP vereint Daten und Funk­tio­nen zu Objekten. Ein Objekt ist quasi eine lebendige Da­ten­struk­tur; denn Objekte sind nicht inert, sondern haben ein Verhalten. Objekte sind somit ver­gleich­bar mit Maschinen oder ein­zelli­gen Or­ga­nis­men. Während auf Daten bloß operiert wird, in­ter­agiert man mit Objekten bzw. in­ter­agie­ren Objekte mit­ein­an­der.

Ver­an­schau­li­chen wir uns den Un­ter­schied an einem Beispiel. Eine Integer-Variable in Java oder C++ enthält lediglich einen Wert. Es handelt sich nicht um eine Da­ten­struk­tur, sondern einen „Primitive“:

int number = 42;

Ope­ra­tio­nen auf Pri­mi­ti­ves erfolgen über Ope­ra­to­ren oder Funk­tio­nen, die außerhalb definiert sind. Hier am Beispiel der successor-Funktion, die auf eine Ganzzahl folgende Zahl liefert:

int successor(int number) {
    return number + 1;
}
// returns `43`
successor(42)

Im Gegensatz dazu gilt in Sprachen wie Python und Ruby: „ever­y­thing is an object“ („alles ist ein Objekt“). Selbst eine einfache Zahl umfasst den ei­gent­li­chen Wert sowie eine Menge von Methoden, die Ope­ra­tio­nen auf dem Wert de­fi­nie­ren. Hier am Beispiel der ein­ge­bau­ten succ-Funktion in Ruby:

# returns `43`
42.succ

Zunächst ist dies praktisch, da die Funk­tio­na­li­tät für einen Datentyp gebündelt wird. Es ist nicht möglich, eine Methode auf­zu­ru­fen, die nicht zum Typ passt. Doch Methoden können noch mehr. In Ruby wird selbst die For-Schleife als Methode einer Zahl rea­li­siert. Wir geben ex­em­pla­risch die Zahlen von 51 bis 42 aus:

51.downto(42) { |n| print n, ".. " }

Woher stammen nun die Methoden? Objekte werden in den meisten Sprachen über Klassen definiert. Man sagt, dass Objekte aus Klassen in­stan­zi­iert werden, und nennt Objekte daher auch Instanzen. Eine Klasse ist eine Schablone zum Erzeugen gleich­ar­ti­ger Objekte, die über dieselben Methoden verfügen. Somit fungieren Klassen in reinen OOP-Sprachen als Typen. Deutlich wird das bei der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung in Python; die type-Funktion liefert als Typ eines Werts eine Klasse zurück:

type(42) # <class 'int'=""></class>
type('Walter White') # <class 'str'=""></class>

Wie funk­tio­niert ob­jekt­ori­en­tier­te Pro­gram­mie­rung?

Fragt man eine Person mit ein paar Semestern Pro­gram­mier­erfah­rung, worum es bei OOP geht, ist die Antwort wahr­schein­lich „irgendwas mit Klassen“. Tat­säch­lich sind Klassen jedoch nicht der Kern der Sache. Die Grund­ideen der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung von Alan Kay sind simpler und lassen sich wie folgt zu­sam­men­fas­sen:

  1. Objekte kapseln ihren internen Zustand.
  2. Objekte empfangen Nach­rich­ten über ihre Methoden.
  3. Die Zuordnung der Methoden erfolgt dynamisch zur Laufzeit.

Diese drei kri­ti­schen Punkte schauen wir uns im Folgenden genauer an.

Objekte kapseln ihren internen Zustand

Um zu verstehen, was mit Kapselung (engl. en­cap­su­la­ti­on) gemeint ist, nutzen wir das Beispiel eines Autos. Ein Auto hat einen be­stimm­ten Zustand, z. B. die Bat­te­rie­la­dung, der Füllstand des Tanks, ob der Motor läuft oder nicht. Wenn wir ein solches Auto als Objekt abbilden, sollen sich die internen Ei­gen­schaf­ten aus­schließ­lich über de­fi­nier­te Schnitt­stel­len ändern lassen.

Schauen wir uns ein paar Beispiele an. Wir haben ein Objekt car, das ein Auto re­prä­sen­tiert. Im Inneren des Objekts wird der Zustand in Variablen ge­spei­chert. Das Objekt verwaltet die Werte der Variablen; so lässt sich bei­spiels­wei­se si­cher­stel­len, dass zum Starten des Motors Energie ver­braucht wird. Wir starten den Motor des Autos, indem wir eine Nachricht start senden:

car.start()

An diesem Punkt ent­schei­det das Objekt darüber, was als nächstes passiert: Läuft der Motor schon, wird die Nachricht ignoriert oder es wird eine ent­spre­chen­de Meldung aus­ge­ge­ben. Ist nicht genug Bat­te­rie­la­dung vorhanden oder ist der Tank leer, bleibt der Motor aus. Sind alle Vor­aus­set­zun­gen erfüllt, wird der Motor gestartet und der interne Zustand angepasst. Etwa wird eine boolesche Variable motor_running auf „True“ gesetzt und die Bat­te­rie­la­dung um die zum Starten benötigte Ladung ver­rin­gert. Wir zeigen sche­ma­tisch, wie der Code im Inneren des Objekts aussehen könnte:

# starting car
motor_running = True
battery_charge -= start_charge

Wichtig ist, dass der interne Zustand von außen nicht direkt ver­än­der­bar ist. Ansonsten könnten wir motor_running auch bei leerer Batterie auf „True“ setzen. Das wäre Magie und würde die tat­säch­li­chen Ge­ge­ben­hei­ten der Realität nicht wi­der­spie­geln.

Nach­rich­ten senden / Methoden aufrufen

Wie wir gesehen haben, reagieren Objekte auf Nach­rich­ten und ändern als Reaktion ggf. ihren internen Zustand. Wir nennen diese Nach­rich­ten Methoden; technisch gesehen handelt es sich dabei um Funk­tio­nen, die an ein Objekt gebunden sind. Die Nachricht besteht aus dem Namen der Methode und ggf. weiteren Ar­gu­men­ten. Das emp­fan­gen­de Objekt wird als Receiver be­zeich­net. Wir drücken das generelle Schema des Nach­rich­ten-Empfangs durch Objekte wie folgt aus:

# call a method
receiver.method(args)

Ein weiteres Beispiel: Stellen wir uns vor, wir pro­gram­mie­ren ein Smart­phone. Ver­schie­de­ne Objekte re­prä­sen­tie­ren Funk­tio­na­li­tä­ten, z. B. die Telefon-Funk­tio­nen die Ta­schen­lam­pe, einen Anruf, eine Text­nach­richt etc. Üb­li­cher­wei­se werden die einzelnen Un­ter­be­stand­tei­le wiederum als Objekte mo­del­liert. So ist das Adress­buch ein Objekt, genau wie jeder ent­hal­te­ne Kontakt und auch die Te­le­fon­num­mer eines Kontakts. So lassen sich Vorgänge aus der Realität leicht mo­del­lie­ren:

# find a person in our address book
person = contacts.find('Walter White')
# let's call that person's work number
call = phone.call(person.phoneNumber('Work'))
...
# after some time, hang up the phone
call.hangUp()

Dy­na­mi­sche Zuordnung der Methoden

Das dritte es­sen­zi­el­le Kriterium in Alan Kays ur­sprüng­li­cher De­fi­ni­ti­on von OOP ist die dy­na­mi­sche Zuordnung der Methoden zur Laufzeit. Das bedeutet, dass die Ent­schei­dung darüber, welcher Code beim Aufruf einer Methode aus­ge­führt wird, erst beim Ausführen des Programms statt­fin­det. Als Kon­se­quenz lässt sich das Verhalten eines Objekts zur Laufzeit mo­di­fi­zie­ren.

Die dy­na­mi­sche Zuordnung der Methoden hat wichtige Aus­wir­kun­gen auf die tech­ni­sche Im­ple­men­ta­ti­on von OOP-Funk­tio­na­li­tät in Pro­gram­mier­spra­chen. In der Praxis hat man damit meist weniger zu tun. Schauen wir uns dennoch ein Beispiel an. Wir mo­del­lie­ren die Ta­schen­lam­pe des Smart­phones als Objekt flash­light. Dieses reagiert auf die Nach­rich­ten on, off und intensity:

// turn on flashlight
flashlight.on()
// set flashlight intensity to 50%
flashlight.intensity(50)
// turn off flashlight
flashlight.off()

Nehmen wir an, die Ta­schen­lam­pe geht kaputt und wir ent­schei­den, bei jeglichem Zugriff eine ent­spre­chen­de Warnung aus­zu­ge­ben. Ein Ansatz besteht darin, alle Methoden durch eine neue Methode zu ersetzen. In Ja­va­Script bei­spiels­wei­se geht das ganz einfach. Wir de­fi­nie­ren die neue Funktion out_of_order und über­schrei­ben damit die exis­tie­ren­den Methoden:

function out_of_order() {
    console.log('Flashlight out of order. Please service phone.')
    return false;
}
flashlight.on = out_of_order;
flashlight.off = out_of_order;
flashlight.intensity = out_of_order;

Versuchen wir im Anschluss, mit der Ta­schen­lam­pe zu in­ter­agie­ren, wird immer out_of_order auf­ge­ru­fen:

// calls `out_of_order()`
flashlight.on()
// calls `out_of_order()`
flashlight.intensity(50)
// calls `out_of_order()`
flashlight.off()

Woher stammen Objekte? In­stan­zi­ie­rung und In­itia­li­sie­rung

Bisher haben wir gesehen, wie Objekte Nach­rich­ten empfangen und darauf reagieren. Doch woher stammen die Objekte? Befassen wir uns nun mit dem zentralen Begriff der In­stan­zi­ie­rung. In­stan­zi­ie­rung ist der Prozess, mit dem ein Objekt ins Leben gerufen wird. In ver­schie­de­nen OOP-Sprachen gibt es un­ter­schied­li­che Me­cha­nis­men der In­stan­zi­ie­rung. Meist kommen ein oder mehrere der folgenden Me­cha­nis­men zum Einsatz:

  1. De­fi­ni­ti­on per Objekt-Literal
  2. In­stan­zi­ie­rung mit Kon­struk­tor-Funktion
  3. In­stan­zi­ie­rung aus einer Klasse

Ja­va­Script glänzt in diesem Punkt, da sich Objekte wie Zahlen oder Strings direkt als Literale de­fi­nie­ren lassen. Ein simples Beispiel: Wir in­stan­zi­ie­ren ein leeres Objekt person und weisen im Anschluss die Ei­gen­schaft name sowie eine Methode greet zu. Im Anschluss ist unser Objekt in der Lage, eine andere Person zu begrüßen und dabei den eigenen Namen zu nennen:

// instantiate empty object
let person = {};
// assign object property
person.name = "Jack";
// assign method
person.greet = function(other) {
    return `"Hi ${other}, I'm ${this.name}"`
};
// let's test
person.greet("Jim")

Wir haben ein ein­zig­ar­ti­ges Objekt in­stan­zi­iert. Häufig möchten wir jedoch die In­stan­zi­ie­rung wie­der­ho­len, um eine Reihe gleich­ar­ti­ger Objekte zu erzeugen. Auch dieser Fall lässt sich in Ja­va­Script leicht abdecken. Wir erzeugen eine so­ge­nann­te Kon­struk­tor-Funktion, die beim Aufruf ein Objekt zu­sam­men­baut. Unsere Kon­struk­tor-Funktion namens Person nimmt einen Namen und ein Alter entgegen und erzeugt beim Aufruf ein neues Objekt:

function Person(name, age) {
    this.name = name;
    this.age = age;
    
    this.introduce_self = function() {
        return `"I'm ${this.name}, ${this.age} years old."`
    }
}
// instantiate person
person = new Person('Walter White', 42)
// let person introduce themselves
person.introduce_self()

Beachten Sie die Benutzung des this-Schlüs­sel­worts. Dieses findet sich auch in anderen Sprachen wie Java, PHP und C++ und sorgt bei OOP-Neulingen oft für Ver­wir­rung. Kurz gesagt ist this ein Platz­hal­ter für ein in­stan­zi­ier­tes Objekt. Beim Aufrufen einer Methode re­fe­ren­ziert this den Receiver, zeigt also auf eine spe­zi­fi­sche Objekt-Instanz. Andere Sprachen wie Python und Ruby benutzen statt this das Schlüs­sel­wort self, das denselben Zweck erfüllt.

Ferner benötigen wir in Ja­va­Script das new-Schlüs­sel­wort, um die Objekt-Instanz korrekt zu erzeugen. Dieses findet sich ins­be­son­de­re in Java und C++, die bei der Spei­che­rung von Werten im Speicher zwischen „Stack“ und „Heap“ un­ter­schei­den. In beiden Sprachen dient new zum Al­lo­zie­ren von Speicher auf dem Heap. Ja­va­Script legt wie Python alle Werte auf dem Heap ab, sodass new ei­gent­lich unnötig ist. Python macht vor, dass es auch ohne geht.

Der dritte und weitaus ver­brei­tets­te Me­cha­nis­mus zum Erzeugen von Objekt-Instanzen bedient sich der Klassen. Eine Klasse erfüllt eine ähnliche Rolle wie eine Kon­struk­tor-Funktion in Ja­va­Script: Beide dienen als Blaupause, nach der sich bei Bedarf gleich­ar­ti­ge Objekte in­stan­zi­ie­ren lassen. Gleich­zei­tig fungiert eine Klasse in Sprachen wie Python und Ruby als Ersatz für die in anderen Sprachen zum Einsatz kommenden Typen. Ein Beispiel für eine Klasse zeigen wir weiter unten.

Was sind die Vor- und Nachteile von OOP?

Die ob­jekt­ori­en­tier­te Pro­gram­mie­rung steht etwa seit Beginn des 21. Jahr­hun­derts verstärkt in der Kritik. Moderne, funk­tio­na­le Sprachen mit Im­mu­ta­bi­li­ty und starken Typ­sys­te­men gelten als stabiler, ver­läss­li­cher und per­for­man­ter. Dennoch findet OOP weite Ver­brei­tung und hat distinkte Vorteile. Wichtig ist, für jedes Problem das richtige Werkzeug zu wählen, anstatt nur auf eine Methodik zu setzen.

Vorteil: Kapselung

Ein un­mit­tel­ba­rer Vorteil von OOP ist die Grup­pie­rung von Funk­tio­na­li­tät. Statt mehrere Variablen und Funk­tio­nen in einer losen Sammlung zu grup­pie­ren, lassen sich diese zu kon­sis­ten­ten Einheiten verbinden. Wir zeigen den Un­ter­schied an einem Beispiel: Wir mo­del­lie­ren einen Bus und nutzen dafür zwei Variablen und eine Funktion. Fahrgäste können den Bus besteigen, bis dieser voll ist:

# list to hold the passengers
bus_passengers = []
# maximum number of passengers
bus_capacity = 12
# add another passenger
def take_bus(passenger)
    if len(bus_passengers) < bus_capacity:
        bus_passengers.append(passenger)
    else:
        raise Exception("Bus is full")

Der Code funk­tio­niert, ist aber pro­ble­ma­tisch. Die take_bus-Funktion greift auf die Variablen bus_pas­sen­gers und bus_capacity zu, ohne dass diese als Argumente übergeben werden. Dies führt bei um­fang­rei­chem Code zu Problemen, da die Variablen entweder global be­reit­ge­stellt oder bei jedem Aufruf übergeben werden müssen. Ferner ist es möglich zu „schummeln“. Wir können dem Bus weiter Pas­sa­gie­re hin­zu­fü­gen, obwohl dieser ei­gent­lich voll ist:

# bus is full
assert len(bus_passengers) == bus_capacity
# will raise exception, won't add passenger
take_bus(passenger)
# we cheat, adding an additional passenger directly
bus_passengers.append(passenger)
# now bus is over capacity
assert len(bus_passengers) > bus_capacity

Zudem hält uns nichts davon ab, die Kapazität des Busses zu erhöhen. Jedoch verletzt dies Annahmen über die physische Realität, denn ein exis­tie­ren­der Bus hat eine begrenzte Kapazität, die sich nach­träg­lich nicht beliebig verändern lässt:

# can't do this in reality
bus_capacity += 1

Das Kapseln des internen Zustands von Objekten schützt vor un­sin­ni­gen oder un­ge­woll­ten Ver­än­de­run­gen. Hier dieselbe Funk­tio­na­li­tät in ob­jekt­ori­en­tier­tem Code. Wir de­fi­nie­ren eine Bus-Klasse und in­stan­zi­ie­ren einen Bus mit li­mi­tier­ter Kapazität. Das Hin­zu­fü­gen von Pas­sa­gie­ren ist nur durch die ent­spre­chen­de Methode möglich:

class Bus():
    def __init__(self, capacity):
        self._passengers = []
        self._capacity = capacity
    
    def enter(self, passenger):
        if len(self._passengers) < self._capacity:
            self._passengers.append(passenger)
            print(f"{passenger} has entered the bus")
        else:
            raise Exception("Bus is full")
# instantiate bus with given capacity
bus = Bus(2)
bus.enter("Jack")
bus.enter("Jim")
# will fail, bus is full
bus.enter("John")

Vorteil: Systeme mo­del­lie­ren

Ob­jekt­ori­en­tier­te Pro­gram­mie­rung eignet sich besonders gut zum Mo­del­lie­ren von System. Dabei ist OOP mensch­lich intuitiv, denn wir denken ebenfalls in Objekten, die sich in Ka­te­go­rien einordnen lassen. Bei den Objekten kann es sich sowohl um physische Dinge handeln als auch um abstrakte Konzepte.

Auch die in vielen OOP-Sprachen an­zu­tref­fen­de Vererbung über Klassen-Hier­ar­chien spiegelt mensch­li­che Denk­mus­ter wider. Ver­an­schau­li­chen wir uns den letzten Punkt an einem Beispiel. Ein Tier ist ein abs­trak­tes Konzept. Tat­säch­lich auf­tre­ten­de Tiere sind immer konkrete Aus­prä­gun­gen einer Spezies. Je nach Spezies haben die Tiere un­ter­schied­li­che Ei­gen­schaf­ten. Ein Hund kann nicht klettern oder fliegen, ist also auf Be­we­gun­gen im zwei­di­men­sio­na­len Raum be­schränkt:

# abstract base class
class Animal():
    def move_to(self, coords):
        pass
# derived class
class Dog(Animal):
    def move_to(self, coords):
        match coords:
            # dogs can't fly nor climb
            case (x, y):
                self._walk_to(coords)
# derived class
class Bird(Animal):
    def move_to(self, coords):
        match coords:
            # birds can walk
            case (x, y):
                self._walk_to(coords)
            # birds can fly
            case (x, z, y):
                self._fly_to(coords)

Nachteile der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung

Ein un­mit­tel­ba­rer Nachteil von OOP ist der anfangs schwer zu durch­schau­en­de Jargon. Man sieht sich gezwungen, ganz neue Konzepte zu erlernen, deren Sinn und Zweck sich an simplen Bei­spie­len oft nicht er­schließt. So un­ter­lau­fen leicht Fehler; gerade die Mo­del­lie­rung von Ver­er­bungs-Hier­ar­chien benötigt eine Menge Geschick und Erfahrung.

Einer der häu­figs­ten Kri­tik­punk­te an OOP ist die ei­gent­lich als Vorteil gedachte Kapselung des internen Zustands. Diese führt zu Schwie­rig­kei­ten beim Par­al­le­li­sie­ren von OOP-Code. Denn wird ein Objekt an mehreren par­al­le­len Funk­tio­nen übergeben, könnte der interne Zustand sich zwischen Funk­ti­ons­auf­ru­fen verändern. Außerdem ist es manchmal notwendig, innerhalb eines Programms auf woanders ge­kap­sel­te In­for­ma­tio­nen zu­zu­grei­fen.

Die dy­na­mi­sche Natur ob­jekt­ori­en­tier­ter Pro­gram­mie­rung führt in der Regel zu Per­for­mance-Einbußen. Denn es lassen sich weniger statische Op­ti­mie­run­gen vornehmen. Auch die ten­den­zi­ell weniger stark aus­ge­präg­ten Typ­sys­te­me reiner OOP-Sprachen machen manche sta­ti­schen Checks unmöglich. So werden Fehler erst zur Laufzeit sichtbar. Neuere Ent­wick­lun­gen wie die Ja­va­Script-Über­spra­che Ty­pe­Script steuern dagegen.

Welche Pro­gram­mier­spra­chen un­ter­stüt­zen oder eignen sich für OOP?

Fast alle Multi-Pa­ra­dig­men-Sprachen eignen sich für ob­jekt­ori­en­tier­te Pro­gram­mie­rung. Dazu gehören die bekannten Internet-Pro­gram­mier­spra­chen PHP, Ruby, Python und Ja­va­Script. Dem­ge­gen­über sind OOP-Prin­zi­pi­en weit­ge­hend un­ver­ein­bar mit der SQL zu­grun­de­lie­gen­den re­la­tio­na­len Algebra. Zum Über­brü­cken des „Impedance Mismatch“ kommen spezielle Über­set­zungs­schich­ten zum Einsatz, die als „Object Re­la­tio­nal Mapper“ (ORM) bekannt sind.

Auch rein funk­tio­na­le Sprachen wie Haskell bringen meist keine native Un­ter­stüt­zung für OOP mit. Um OOP in C um­zu­set­zen, muss man einiges an Aufwand betreiben. In­ter­es­san­ter­wei­se existiert mit Rust eine moderne Sprache, die ohne Klassen auskommt. Statt­des­sen kommen struct und enum als Da­ten­struk­tu­ren zum Einsatz, deren Verhalten per impl-Schlüs­sel­wort definiert wird. Mit so­ge­nann­ten Traits lassen sich Verhalten grup­pie­ren; auch Vererbung und Po­ly­mor­phis­mus werden so ab­ge­bil­det. Das Design der Sprache spiegelt die OOP-Best-Practice „Com­po­si­ti­on over In­he­ri­tance“ wider.

Zum Hauptmenü