Vier Säulen der OOP
Die objektorientierte Programmierung zeichnet sich durch vier Merkmale aus, die oftmals als die vier Säulen der objektorientierten Programmierung bezeichnet werden:
Diese vier Säulen sind (englische Übersetzung in Klammern):
- Kapselung (Encapsulation): Daten und Logik werden in einen gemeinsamen Gültigkeitsbereich zusammengenommen und zu einem neuen Datentyp kombiniert, der die einzelnen Bestandteile unter einem Ganzen kapselt.
- Abstraktion (Abstraction): Programmkonstrukte definieren Operationen auf einer höheren Bedeutungsebene, wodurch Zusammenhänge der realen Welt in einer klareren und präziseren Sprache ausgedrückt werden können.
- Vererbung (Inheritance): Neue Datentypen können die Funktionalität von bestehenden Datentypen übernehmen und dabei erweitern oder verändern. Bei der Vererbung erweitert ein neuer Datentyp einen bestehenden.
- Polymorphie (Polymorphism): Datentypen mit einer gemeinsamen Schnittstelle können spezifisches Verhalten definieren, wodurch sie sich einfacher austauschen lassen. Ein abstrakter Datentyp kann in verschiedenen Gestalten («Polymorphie» besteutet «Vielgestaltigkeit») als konkreter Datentyp umgesetzt werden.
Diese vier Konzepte sollen durch konkrete Beispiele veranschaulicht werden:
Kapselung
Betrachten wir das Beispiel einer Person, die über einen Vor- und Nachnamen verfügt. Diese beiden Eigenschaften – firstName und lastName können in einer Klasse FullName zusammengefasst werden. Die Klasse verfügt über zwei Methoden: formatDefault und formatSwiss, welche den vollen Namen in der Form [Vorname] [Nachname] bzw. schweizerisch als [Nachname], [Vorname] zurückgeben:
Die Klasse kann folgendermassen verwendet werden:
Die beiden Eigenschaften haben die Sichtbarkeit private, d.h. es kann nur innerhalb der Klasse auf sie zugegriffen werden. Die beiden Methoden haben jedoch (implizit, da nichts anderes definiert ist) die Sichtbarkeit public, wodurch man sie auch von aussen verwenden kann. Dadurch sind die Daten gekapselt: Ein direkter Zugriff darauf ist nicht möglich, sondern muss über die beiden Methoden erfolgen.
Man kann die Kapselung somit als ein Verpacken und Verstecken verstehen, so wie ein elektronisches Gerät komplizierte Vorgänge vor dem Anwender versteckt, diesem aber eine vereinfachte Benutzerschnittstelle anbietet.
Abstraktion
Beim Programmieren agiert man auf verschiedenen Bedeutungsebenen oder Abstraktionsebenen. Jede Programmiersprache bietet Grundoperationen wie z.B. die Addition und Multiplikation für Zahlen an. Programme werden aber zumeist erst dann nützlich, wenn sie Funktionen der realen Welt abbilden können.
Betrachten wir als Beispiel einen Zähler. Dieser ist sehr ähnlich wie eine Zahl, hat aber einige Einschränkungen:
Der Zähler…
- … beginnt immer bei 0.
- … kann schrittweise um 1 erhöht werden.
- … kann niemals gesenkt werden.
Trotz oder vielmehr wegen dieser Einschränkungen ist ein Zähler für bestimmte Anwendungsgebiete besser geeignet als eine einfache Zahl. Denn diese könnte beliebig von einem anderen Programmteil verändert werden.
Ein Zähler mit solchen Einschränkungen lässt sich folgendermassen umsetzen:
Wiederum kommt Kapselung zum Einsatz, wodurch der Wert des Zählers (count) nur um Einzelschritte erhöht (increase) und herausgelesen werden kann (getCount). Es ist nicht möglich, den Zähler zurückzusetzen.
Damit lassen sich etwa Besucher auf einer Webseite zählen:
Abstraktionen können zu neuen und höheren Abstraktionen kombiniert werden. Nur durch das Einführen sinnvoller Abstraktionsschichten lässt sich die Komplexität realistischer Programme überhaupt noch bewältigen.
Vererbung
Die Vererbung ist ein Mechanismus, mit der die Funktionalität einer bestehenden Klasse von zusätzlichen Klassen erweitert werden kann. Hierbei hat man es mit einer Vererbungshierarchie zu tun: Eine Klasse ganz oben an der Hierarchie ist sehr allgemein. Die Klassen weiter unten in der Hierarchie, die von den Klassen weiter oben in der Hierarchie erben, fügen auf jeder Stufe Erweiterungen hinzu.
Betrachten wir hierzu ein Beispiel als UML-Klassendiagramm:
---
title: "Vererbung: Angestellte und Kunden sind Spezialfälle von Personen"
---
classDiagram
class Person {
-firstName : string
-lastName : string
+constructor(firstName : string, lastName : string)
+fullName() string
+describe() string
}
class Employee {
-company : string
-boss : Employee
+constructor(firstName : string, lastName : string, company : string, boss : Employee)
+describe() string
}
class Customer {
-revenue : number
+constructor(firstName : string, lastName : string)
+bookSell(amount : number)
+describe() string
}
Person <|-- Employee
Person <|-- Customer
Employee o-- EmployeeDie Vererbungshierarchie besteht aus drei Klassen:
Person: Eine Person mit Vor- (firstName) und Nachnamen (lastName)Employee: Ein Angestellter mit Firma (company) und Vorgesetztem (boss)Customer: Ein Kunde mit Umsatz (amount)
Der Pfeil besagt: die Klasse mit dem ausgehenden Pfeil erbt von bzw. erweitert die Klasse mit dem eingehenden Pfeil. (Die Verbindungslinie bei Employee mit der Raute hat eine andere Bedeutung.)
Sowohl Employee und Customer sind gleichzeitig auch eine Person und verfügen damit über einen Vor- und Nachnamen. Auch die Methoden fullName und swissName lassen sich auf Instanzen aller drei Klassen aufrufen.
Die Implementierung sieht folgendermassen aus. Zuerst die Klasse Person:
Die Klasse ist praktisch identisch zur Klasse FullName weiter oben.
Ein Angestellter (Employee) ist folgendermassen implementiert:
Der Vorgesetzte ist eine optionale Eigenschaft. Je nachdem, ob ein Angestellter über einen Vorgesetzten verfügt, gibt die Methode describe() eine andere Beschreibung der Person zurück.
Der Kunde (Customer) ist folgendermassen implementiert:
Mit der Methode bookSell kann ein Verkauf verbucht werden, der dem Umsatz zugeschlagen wird. Dieser Umsatz wird dann von der Methode describe ausgegeben.
Die Klassen lassen sich folgendermassen verwenden:
Die Ausgabe wird folgendermassen aussehen:
Polymorphie
Sowie die Kapselung die Abstraktion ermöglicht, ist auch die Vererbung die Grundlage für einen weiteren Mechanismus: die Polymorphie bzw. die Vielgestaltigkeit.
Bei der Polymorphie unterscheidet man zwischen einem abstrakten Datentyp einerseits und einem oder mehreren konkreten Datentypen andererseits. Der abstrakte Typ tritt durch konkrete Typen in verschiedenen Gestalten zur Erscheinung.
Betrachten wir als Beispiel die Grundrechenoperationen. Auf einer abstrakten Ebene kombinieren diese jeweils zwei Operanden mit einem Operator. Ein Wert kann von einer Reihe von Operationen verarbeitet werden. Dies lässt sich folgendermassen als UML-Klassendiagramm darstellen:
---
title: "Polymorphie: Eine abstrakte Operation kann verschiedene konkrete Formen annehmen"
---
classDiagram
class Value {
-state : number
+constructor(value : number)
+calculate(operations : Array~Operation~) Value
+value() number
}
class Operation {
-operand : number
+constructor(operand : number)
+perform(x : number) number
}
class Add {
}
class Sub {
}
class Mul {
}
class Div {
}
Operation <|-- Add
Operation <|-- Sub
Operation <|-- Mul
Operation <|-- Div
Value --> Value
Value --> OperationEs gibt eine Klasse Operation, welche mithilfe eines Operanden erzeugt wird, und die diesen als Eigenschaft operand abspeichert. Mit der Methode perform, welche einen weiteren Operanden als Parameter erhält, führt sie ihre Arbeit aus.
Die Klasse Value kapselt eine einfache Zahl als Zustand (state). Ihre Methode calculate erwartet eine Reihe von konkreten Operationen und gibt das Resultat als neue Instanz von Value zurück. Die Zahl erhält man durch den Aufruf der Methode value() zurück. Diese Klasse ist folgendermassen implementiert:
Die Methode calculate arbeitet die gegebenen Operationen eine um die andere ab, merkt sich das Zwischenresultat und gibt schliesslich das Endresultat zurück.
Die Klasse Operation ist als abstrakte Klasse implementiert:
Der Operand hat die Sichtbarkeit protected: Diese Eigenschaft kann nur innerhalb der Klasse Operation und innerhalb der Klassen verwendet werden, welche von Operation erben. Die folgenden vier Klassen implementieren je eine Rechenoperation, wozu sie auf operand zugreifen:
Im folgenden Beispiel wird, ausgehend von einem Wert von 7, eine Reihe von Rechenoperationen ausgeführt:
Das Ergebnis:
$$ (((7 + 1) : 2) - 1) * 2 = 6 $$Die abstrakte Klasse Operation tritt als verschiedene konkrete Klassen (Add, Sub, Mul, Div) in Erscheinung. Die konkreten Klassen sind dank der Polymorphie austauschbar.