Objektorientiertes Design
Ziele
- Ich kann die Konzepte der Generalisierung und Spezialisierung im Kontext der Vererbung erklären und anwenden.
- Ich weiss, wie ich Unterklassen von einer Oberklasse ableiten kann (extends).
- Ich kann vererbte Methoden und Attribute einer Klasse von lokalen Methoden und Attributen unterscheiden.
- Ich kann in UML (Unified Modeling Language) die Vererbungsbeziehung zweier Klassen erkennen.
- Ich weiss, wie ich das Schlüsselwort
super
nutze, um den Konstruktor der Oberklasse zu verwenden. - Ich weiss, wie ich das Schlüsselwort
super
nutze, um eine Methode aus der Oberklasse aufzurufen. - Ich kann das Konzept des “Überschreibens” (Overriding) von Methoden und Attributen erklären, erkennen und nutzen.
- Ich weiss, was Interfaces sind und wann es angebracht ist, ein Interface zu verwenden.
- Ich weiss, welche Methoden und Felder in einem Interface vorhanden sind und wie sie geschrieben werden bzw. über welche Eigenschaften sie verfügen.
- Ich kann eine Klasse schreiben, die ein Interface implementiert.
Einführung
Beim objektorientierten Design (OOD) handelt es sich um die Modellierung der realen Welt in Klassen und Objekten. In dieser Phase der Softwareentwicklung werden Objekte und Klassen definiert sowie ihre Eigenschaften, Funktionen und die Beziehungen untereinander festgelegt.
Die wichtigsten Konzepte/Prinzipien des objektorientierten Designs sind:
- Vererbung ermöglicht es in Java, eine hierarchische Ordnung für Klassen festzulegen. Dadurch wird die Menge an redundantem Code reduziert.
- Polymorphismus beschreibt eine Sprachstruktur, bei der Methoden mit derselben Signatur unterschiedlich implementiert werden können und dadurch verschiedene Ergebnisse liefern.
- Abstraktion ist ein Prinzip, bei dem durch das Weglassen von Details nur die wesentlichen Eigenschaften eines Objekts hervorgehoben werden.
- Kapselung ermöglicht es, den Zugriff auf die Methoden und Attribute einer Klasse zu kontrollieren und zu schützen.
Das Befolgen dieser Konzepte/Prinzipien führt zu einem guten objektorientierten Design und trägt wesentlich zur Qualität der Software bei.
Vererbung
Vererbung ist ein Mechanismus zum Ableiten einer neuen Klasse von einer anderen Klasse. Die neue Klasse erbt alle nicht-privaten Felder und Methoden der Basisklasse. Die Vererbung ist eines der Hauptkonzepte der objektorientierten Programmierung.
Als Beispiel dient uns die Klasse Auto, welche von der Klasse Fahrzeug abgeleitet ist. Die Beziehung zwischen den Klassen wird als IS-A Beziehung bezeichnet. Wir würden also logischerweise sagen, dass ein Auto ein Fahrzeug ist. Die Klasse Auto wird auch als Spezialisierung der Klasse Fahrzeug bezeichnet. Umgekehrt bezeichnen wir die Klasse Fahrzeug als Generalisierung der Klasse Auto.
Synonyme für die abgeleitete Klasse sind:
- Unterklasse (Subclass),
- abgeleitete Klasse (Derived Class),
- erweiterte Klasse (Extended Class),
- Kind-Klasse (Child Class).
Synonyme für die Klasse, von der abgeleitet wird:
- Oberklasse (Superclass),
- Basisklasse (Base Class),
- Eltern-Klasse (Parent Class).
Der Anwendungsfall der Vererbung kommt also dort zum Tragen, wo es eine IS-A Beziehung zwischen zwei Objekten gibt. Dazu ein paar Beispiele:
- Ein Quadrat ist eine geometrische Form.
- Java ist eine Programmiersprache.
- Ein Schwert ist eine Klingenwaffe.
- Eine Klingenwaffe ist eine Waffe.
Superklasse | Subklasse |
---|---|
Geometrische Form | Quadrat |
Programmiersprache | Java |
Klingenwaffe | Schwert |
Waffe | Klingenwaffe |
Es gibt einige wichtige Punkte zur Vererbung in Java:
- In Java gibt es keine Mehrfachvererbung. Eine Klasse kann immer nur von maximal einer anderen Klasse erben
- Eine Klassenhierarchie kann beliebig viele Ebenen haben
- Die Klasse Schwert erbt von der Klasse Klingenwaffe und die Klasse Klingenwaffe erbt von der Klasse Waffe
- Eine Superklasse kann beliebig viele Subklassen haben
Im UML-Diagramm sind die Basisklassen oberhalb der abgeleiteten Klassen abgebildet. Die Klassen werden mit Pfeilen verbunden, wobei die Pfeilrichtung von der abgeleiteten Klasse in Richtung der Basisklasse verläuft. Der Vererbungspfeil hat eine durchgezogene Linie und ein geschlossenes Dreieck als Pfeilspitze.
Eine Subklasse kann beliebig viele neue Felder und Methoden enthalten. Geerbte und neu hinzugefügte Felder und Methoden werden wie bisher gelernt verwendet.
Das Schlüsselwort extends
In Java wird eine Vererbungsbeziehung implementiert, indem wir das Schlüsselwort extends
verwenden.
|
|
|
|
Bei der Deklaration eines Autos ist es nun aufgrund der Vererbungsbeziehung möglich, dass wir statt eines Autos ein Fahrzeug verwenden. Dies funktioniert, weil ein Auto ja ein Fahrzeug ist (IS-A).
|
|
Das Schlüsselwort final
Wenn eine Klasse mit dem Schlüsselwort final
versehen wird, dann kann sie keine Subklassen haben.
Wir können die Vererbung also verbieten.
|
|
Viele der Standardklassen von Java sind final. Dazu gehören alle Wrapper-Klassen von primitiven Datentypen wie Integer, Long oder Float und die Klasse String.
Das Schlüsselwort super
Das Schlüsselwort super
ähnelt dem Schlüsselwort this
. Es erlaubt den direkten Zugriff auf
Felder, Konstruktoren und Methoden der Superklasse. Bei gleicher Namensgebung von Feldern oder beim
Überschreiben von Methoden ist es teilweise sogar zwingend notwendig.
Umgang mit Konstruktoren
Konstruktoren werden nicht an die Subklasse vererbt. Wenn aber ein neues Objekt einer Subklasse erzeugt werden soll, so kann der Konstruktor der Superklasse nicht einfach ignoriert werden. Beim Erzeugen von Objekten einer Subklasse unterscheiden wir zwischen den folgenden Fällen:
- Die Superklasse hat keinen Konstruktor (das heisst, sie besitzt einen Default-Konstruktor).
- Die Superklasse hat einen anderen Konstruktor als den Default-Konstruktor.
- Die Superklasse hat einen anderen Konstruktor und zusätzlich einen Default-Konstruktor.
Beispiel 1 - Die Superklasse hat keinen Konstruktor
|
|
|
|
|
|
Beispiel 2 - Die Superklasse hat einen anderen Konstruktor als den Default-Konstruktor
|
|
|
|
|
|
Für die Erzegung eines Fahrzeugs ist nun eine Marke notwendig. Dies bedeutet automatisch, dass die Erzeugung eines Autos auch einen Wert für diese Marke besitzen muss. Der Wert kann entweder über einen weiteren Konstruktor in der Klasse Auto in das Fahrzeug gelangen oder man wählt - wie im Beispiel gezeigt - einen fixen Wert. Die Erzeugung eines neuen Autos ohne einen Wert für die Marke ist aber nicht möglich, da die Superklasse einen Wert verlangt.
Beispiel 3 - Die Superklasse hat einen anderen Konstruktor und zusätzlich einen Default-Konstruktor
|
|
|
|
|
|
Hier gelten die gleichen Regeln wie beim Beispiel 2. Der einzige Unterschied besteht nun darin, dass die Klasse Auto ebenfalls beide Konstruktoren besitzen muss.
Der geübte Entwickler behält also stets die Konstruktoren der Superklasse im Auge. Sie werden immer vor den Konstruktoren der Subklasse aufgerufen. Dies ist auch der Grund, weshalb der Aufruf des Super-Konstruktors immer als erstes Statement in einem Subklassen-Konstruktor aufgeführt werden muss.
Beziehungen
In Java gibt es vier Grundtypen von Beziehungen, welche Objekte miteinander bilden können. Diese sind:
- Generalisierung und Spezialisierung (IS-A Beziehung)
- Aggregation und Komposition (HAS-A Beziehung)
- Assoziationen (KNOWS-A Beziehung)
- Abhängigkeit (USES Beziehung)
Generalisierung und Spezialisierung (IS-A Beziehung)
Die IS-A Beziehung beschreibt, wovon sich ein Objekt ableitet. Dies gilt für Basisklassen, abstrakte Klassen und Interfaces. Je genereller eine Funktion oder Beschreibung ist, desto höher ist sie stets in der Klassenhierarchie. Weiter unten in der Hierarchie sind also die spezialisierten Dinge anzutreffen.
Darstellung der Generalisierung mit UML:
|
|
Bei der Implementation eines Interfaces mit UML wird die folgende Darstellung verwendet (Interfaces werden weiter unten erklärt):
|
|
Aggregation und Komposition (HAS-A Beziehung)
Die HAS-A Beziehung beschreibt, woraus sich ein Objekt zusammensetzt. Ein Objekt kann selbstverständlich beliebig viele andere Objekte aufnehmen. Die aufgenommenen Objekte sind dabei Bestandteile des Hauptobjekts.
Ein einfaches Beispiel wäre, dass ein Auto (normalerweise) einen Motor hat.
|
|
|
|
Die Umsetzung beider Beziehungen wird durch Instanzvariablen abgebildet, welche die entsprechenden
Objekte aufnehmen.
Ist bei einer Aggregation das verbundene Objekt nicht vorhanden, so wird der Instanzvariable der
Wert null
zugewiesen.
Wenn die Beziehung zwischen den Objekten mehrfach (1 zu n) vorhanden ist, so kann dafür ein Array
oder auch eine Liste verwendet werden.
Bei dieser Beziehung wird zwischen Aggregation und Komposition unterschieden.
Aggregation Die Aggregation ist
- stärker als eine Assoziation (siehe weiter unten), aber schwächer als eine Komposition.
- eine Beziehung der Art “besitzt ein/e”.
- in ihrer Lebensdauer nicht an die Lebensdauer des Ganzen gebunden.
Beispiel 1: “Eine Taskforce hat Experten und -innen”. Das bedeutet, dass es die Experten und -innen immer noch gibt, wenn die Taskforce aufgelöst wird. Beispiel 2: “Ein Auto hat einen Fahrer oder eine Fahrerin”. Die Existenz des Fahrers / der Fahrerin ist nicht an die Existenz des Autos gebunden.
Darstellung der Aggregation mit UML:
In der Aggregation kann das zugeordnete Objekt unabhängig existieren. Es wird also nicht vollständig vom “Besitzer” (Container-Objekt) kontrolliert.
|
|
Hier kann der Fahrer unabhängig vom Auto existieren, was die lose Beziehung der Aggregation verdeutlicht.
Komposition Die Komposition ist
- eine sehr starke Beziehung.
- eine Beziehung der Art “ist ein Teil von” / “besteht aus”.
- in ihrer Lebensdauer an die Lebensdauer des Ganzen gebunden.
Beispiel 1: “Ein Labyrinth hat Wände.” Eine Wand kann nur als Teil eines Labyrinths existieren. Beispiel 2: “Ein Mensch hat ein Herz.” Ein Mensch kann ohne Herz nicht existieren.
Darstellung der Komposition in UML:
In der Komposition existiert das “Teil”-Objekt nur im Kontext des “Ganzen”. Es kann nicht unabhängig existieren.
|
|
Hier ist Raum vollständig in Haus eingebettet, und ohne das Haus gäbe es den Raum nicht, was die Komposition darstellt. Die Instanz von Raum wird direkt im Haus erstellt und existiert ausserhalb nicht.
Assoziation (KNOWS-A Beziehung)
Wir haben bereits zwei Formen von Assoziationen kennengelernt: Aggregation & Komposition. Wenn von einer Assoziation die Rede ist, so sind damit Objekte gemeint, welche miteinander auf irgendeine Weise in Beziehung stehen. Die Komposition ist die stärkste Form der Assoziation, die Aggregation ist etwas abgeschwächt und die Assoziation selbst ist die schwächste Beziehung. Der Begriff Assoziation ist hier etwas verwirrend, weil er gleichzeitig als Oberbegriff und als Verbindung benutzt wird.
Eine Assoziation ist
- eine Beziehung der Art “benutzt ein/e”, “ist zugeordnet zu”, “hat eine Beziehung zu”.
- auch unter der Bezeichnung KNOWS-A bekannt.
Beispiel: “Eine Musikerin spielt ein Instrument.” Sie “kennt” das Instrument, das sie spielt.
Darstellung der Assoziation in UML:
Die Assoziation beschreibt eine lose Beziehung zwischen Objekten, die miteinander “wissen” oder " kennen".
|
|
Der Musiker kennt das Instrument, das er spielt, was die lose Assoziation verdeutlicht.
Gerichtete Assoziation
Eine gerichtete Assoziation ist eine Verbindung zwischen zwei Klassen, bei der die Beziehung eine klare Richtung hat.
Beispiel: Du hast zwei Klassen: Kunde und Bestellung. Es besteht eine gerichtete Assoziation von Kunde zu Bestellung. Das bedeutet, dass der Kunde eine oder mehrere Bestellungen kennt, Bestellung weiss aber nichts über den Kunden.
Eine gerichtete Assoziation zeigt eine Richtung in der Beziehung zwischen zwei Objekten.
|
|
Hier “kennt” der Kunde die Bestellung, aber die Bestellung weiss nichts vom Kunden.
Abhängigkeit (USES Beziehung)
Eine Abhängigkeit ist
- eine gerichtete Beziehung zwischen einem abhängigen (Client) und einem unabhängigen Element ( Supplier).
- eine Beziehung, wo die eine Klasse die andere zum Funktionieren braucht.
- schwächer als eine Assoziation.
- möglich, ohne ein Objekt der Abhängigkeit dauerhaft zu speichern.
Die abhängige Klasse hat keine Instanzvariable vom Typ der unabhängigen Klasse. Es werden nur Parameter vom Typ der unabhängigen Klasse verwendet. Es ist auch möglich, eine Abhängigkeit ohne Objekte zu erstellen, zum Beispiel mit statischen Methoden.
Darstellung der Abhängigkeit in UML:
Eine Abhängigkeit zeigt, dass eine Klasse eine andere zum Funktionieren benötigt, aber nicht dauerhaft hält.
|
|
In diesem Beispiel benötigt der Benutzer einen Drucker, um ein Dokument zu drucken, aber es gibt
keine dauerhafte Beziehung, nur eine temporäre Verwendung.
Es gibt also keine Instanzvariable des Typs Drucker
in Benutzer
, sondern nur eine Methode mit
einem Parameter drucker
, welcher nur temporär existiert.
Polymorphismus
Polymorphie bedeutet “Vielgestaltigkeit”. Die Polymorphie beschreibt ein Konzept der
objektorientierten Programmierung, wobei der Aufruf einer Methode mit identischer Signatur
unterschiedliche Ergebnisse liefern kann. Dieses Verhalten ist vorallem bei der Vererbung
anzutreffen.
In Java sind alle Objekte polymorph, da jedes Objekt eine IS-A Beziehung für seinen eigenen Typ und
für die Klasse Object
besitzt (alle Klassen erben von Object, Everything is an Object).
Eine Referenzvariable kann auf jedes Objekt ihres deklarierten Typs oder auf jeden Subtyp ihres deklarierten Typs verweisen.
Beispiel:
|
|
|
|
|
|
Die Ausgabe ist wie folgt:
Animals can move
Dogs can walk and run
In diesem Beispiel ist erkennbar, dass die Referenz dog
(obwohl es sich um ein Animal
handelt),
die Methode move() der Klasse Dog
ausführt.
Der Grund dafür ist, dass während der Kompilierung der Referenztyp überprüft wird.
Zur Laufzeit ermittelt die JVM jedoch das Objekt und führt die Methode aus, die zu der Klasse dieses
Objekts gehört.
Overriding
Beim Überschreiben von Methoden wird eine Methode der Superklasse in einer Subklasse neu definiert. Eine Subklasse kann dadurch das Verhalten einer Methode der Superklasse anders spezifizieren. Das Überschreiben hat den Vorteil, dass ein Verhalten definiert werden kann, das für den Typ der Subklasse spezifisch ist. Die überschreibende Methode muss dieselbe Signatur (Methodenname; Anzahl, Typ und Reihenfolge der Parameter) aufweisen. Die zu überschreibende Methode darf nicht final sein.
Beispiel:
|
|
|
|
|
|
|
|
Die Ausgaben sind nicht verwunderlich. Da es sich um Objekte des Typs Rectangle und Circle handelt, werden jeweils die überschriebenen Methoden aufgerufen. Wenn eine Subklasse ein bestimmte Methode nicht überschreibt, so wird die Methode der nächsthöheren Klasse (in diesem Falle der Klasse Shape) verwendet.
@Override
Die Annotation @Override
weist den Compiler an, die Signatur der überschreibenden Methode zu
überprüfen.
Die Annotation ist optional, hilft aber in einfacher Weise Fehler beim Überschreiben zu verhindern.
Wenn eine mit @Override
gekennzeichnete Methode die Methode der Superklasse nicht korrekt
überschreibt, generiert der Compiler einen Fehler.
Abstraktion
In der objektorientierten Programmierung bezieht sich Abstraktion darauf, dem Benutzer Funktionalität bereitzustellen, ohne die Details der Implementierung preiszugeben. Es ist also bekannt, was ein Objekt tun kann, aber nicht wie die Funktionalität genau umgesetzt ist. In Java wird Abstraktion mit Hilfe von abstrakten Klassen und Interfaces erreicht.
Abstrakte Klassen
Eine Klasse, die das Schlüsselwort abstract
in ihrer Deklaration enthält, wird als abstrakte
Klasse bezeichnet.
- Abstrakte Klassen können beliebig viele abstrakte Methoden enthalten.
- Eine abstrakte Methode besitzt keinen Block, sie muss in jedem Fall durch eine nicht-abstrakte Methode einer Klasse in der darunterliegenden Hierarchie überschrieben werden.
- Eine abstrakte Klasse kann nicht instanziert werden, es ist also nicht möglich von einer solchen Klasse ein Objekt zu erstellen.
- Abstrakte Klassen eignen sich, um gemeinsame Funktionalitäten von Subklassen aufzunehmen.
Beispiel:
- Eine abstrakte Klasse
Animal
- Eine abstrakte Subklasse
Carnivore
(Fleischfresser) - Eine abstrakte Subklasse
Herbivore
(Pflanzenfresser) - Eine Subklasse
Dog
- Eine Subklasse
Cat
- Eine Subklasse
Sheep
- Eine Subklasse
Cow
Alle diese Tiere sollen sich bewegen und unterschiedliche Geräusche machen können. Die Methoden move() und sound() bewerkstelligen dies.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Zusammenfassung:
- Alle Tiere können sich bewegen. Als ein gemeinsames Merkmal ist dies in der Klasse
Animal
implementiert. - Fleischfresser und Pflanzenfresser könnten in den jeweiligen Klassen spezifische Implementationen bereitstellen.
- Alle Tiere machen unterschiedliche Geräusche und aus diesem Grund wird die Methode sound() in der
Klasse
Animal
alsabstract
deklariert, so dass alle untergeordneten Klassen diese Methode auf ihre eigene Weise implementieren müssen.
Eine weitere wichtige Lektion ist die Polymorphie in der Klassenhierarchie. Eine Katze ist gemäss Definition ein Fleischfresser. Das folgende Beispiel soll die mögliche Typenumwandlung erklären.
|
|
Wie wir sehen ist die Umwandlung in einen Typ, welche höher in der Klassenhierarchie liegt, stets
ohne Cast-Operator möglich. Bei der Umwandlung in einen unterliegenden Typ (Downcasting) muss der
Cast-Operator zwingend implementiert werden.
Zur Laufzeit kann es beim Downcasting jedoch zu einer ClassCastException
kommen, wenn die Referenz
kein Objekt des gecasteten Typs ist.
instanceof Operator
Durch den Einsatz des Operators instanceof kann zur Laufzeit die Referenz eines Objektes auf einen bestimmten Typ überprüft werden.
Beispiel:
|
|
Der Operator überprüft also den Typ einer Instanz und berücksichtigt dabei Subklassen und Interfaces.
Interfaces
Ein Interface dient dem Angebot von Methoden, die durch Klassen zu implementieren sind, welche das
Interface “implementieren”. Damit definiert ein Interface einen Satz von bestimmten Funktionen, die
allen implementierenden Klassen des Interfaces gleich sind. Ein Interface muss dabei nicht zwingend
eine Methode enthalten. Dies ist z.B. der Fall beim Interface Serializable
. Dieses sagt lediglich
semantisch aus, dass eine Klasse resp. das Objekt davon serialisiert werden kann.
Eine Schnittstelle hat im Unterschied zu einer Klasse weder ein Verhalten noch einen Status – wir
können ein Interface als einen Vertrag betrachtet, den eine Klasse erfüllen muss.
Ein Interface besitzt anstelle der Klassendefinition das Schlüsselwort interface
.
Ein Interface kann die folgenden Dinge enthalten:
- Konstanten, also
public static final
Variablen, wobei die Schlüsselwörter nicht erforderlich sind. public abstract
Methoden, wobei die Schlüsselwörte nicht erforderlich sind.- Normale Methoden mit Implementierung (das Schlüsselwort
default
ist erforderlich) seit Java 8. - Statische Methoden mit Implementierung (das Schlüsselwort
static
ist erforderlich) seit Java 8.
Ein Interface darf die folgenden Dinge nicht enthalten:
- Instanzvariablen
- Konstruktoren
- Nicht-öffentliche abstrakte Methoden
|
|
Eine Klasse verwendet in ihrer Deklaration das Schlüsselwort implements
, um eine Schnittstelle zu
implementieren.
Ein Interface verwendet jedoch das Schlüsselwort extends
, um eine andere Schnittstelle zu
erweitern.
Beispiel:
- Eine Basisklasse mit dem Namen
Bird
- Eine Subklasse mit dem Namen
Parrot
- Eine Subklasse mit dem Namen
Penguin
- Ein Interface mit dem Namen
Flyable
|
|
|
|
|
|
|
|
Wie wir sehen ist die Klasse Parrot
gezwungen den Vertrag mit dem Interface Flyable
zu erfüllen.
Der Vorteil dieser Implementation wird erst ersichtlich, wenn das Interface beispielsweise als
Parameter verwendet wird. Nur Instanzen von Klassen, welche das Interface implementieren, können als
Parameter verwendet werden.
|
|
|
|
Im Beispiel sehen wir, dass durch die Verwendung eines Interfaces die Abhängigkeiten zwischen den
Klassen Birdhouse
und Parrot
vollständig aufgehoben wird.
Beide Klassen kennen einander nicht, dies wird Entkopplung genannt. Dem Vogelhaus ist es also egal,
welcher Vogel ankommt. Er muss aber fliegen können.
Komposition vor Vererbung
“Komposition vor Vererbung” ist ein Prinzip in der objektorientierten Programmierung (OOP), das empfiehlt, Komposition gegenüber Vererbung zu bevorzugen, um Systeme zu entwerfen. Dieses Prinzip zielt darauf ab, die Einschränkungen der Vererbung zu überwinden und flexibleren sowie wartbareren Code zu ermöglichen.
Lass uns die beiden Konzepte näher betrachten:
Vererbung
Vererbung ist ein Mechanismus, bei dem eine Klasse (die Unterklasse oder abgeleitete Klasse)
Attribute und Methoden von einer anderen Klasse (der Oberklasse oder Basisklasse) erbt. Vererbung
ermöglicht die Wiederverwendung von Code und stellt Beziehungen wie “ist-ein” (z. B. ein Hund ist
ein Säugetier
) her.
Die Vererbung kann jedoch einige Probleme verursachen:
- Enge Kopplung: Unterklassen sind eng mit ihren Oberklassen verbunden, was bedeutet, dass Änderungen in der Oberklasse alle abgeleiteten Klassen beeinflussen können.
- Starre Hierarchie: Vererbung schafft eine strenge Hierarchie, die oft unflexibel ist. Es wird schwieriger, neue Funktionalitäten hinzuzufügen, ohne bestehende Strukturen zu ändern. Dies kann gegen das Open-Closed-Prinzip (OCP) des SOLID-Designs verstossen.
- Übernutzung führt zu Zerbrechlichkeit: Zu viel Vererbung kann zu fragilen und schwer wartbaren Codebasen führen. Änderungen in einer Oberklasse können unvorhergesehene Konsequenzen für alle abgeleiteten Klassen haben.
Komposition
Komposition hingegen bezieht sich auf die Praxis, eine Klasse durch den Einsatz anderer Klassen zu "
komponieren", anstatt Vererbung zu verwenden. Das bedeutet, dass eine Klasse Objekte von anderen
Klassen als Instanzvariablen enthält, um Funktionalität wiederzuverwenden. Dies folgt dem “hat-ein”
-Prinzip (z. B. ein Auto
hat einen Motor
).
Vorteile der Komposition:
- Flexibilität: Komposition ermöglicht es, Objekte dynamisch zu kombinieren oder zu ändern, was mehr Flexibilität bei der Strukturierung eines Programms bietet.
- Geringere Kopplung: Da Klassen nicht voneinander erben, sind sie weniger eng miteinander verbunden. Änderungen in einer Klasse wirken sich nicht auf die andere aus.
- Erleichtert das Testen: Da Klassen unabhängiger sind, wird das Testen vereinfacht, insbesondere bei der Verwendung von Mock-Objekten oder Stubs.
Beispiel
Anstatt eine Klasse Vogel von einer Klasse Tier zu erben, könnte man eine Klasse Fliegen als Komponente verwenden:
Vererbung:
|
|
Das Problem hier ist, wenn es ein Vogel gibt, welcher nicht fliegen kann, wie zum Beispiel ein Kiwi.
Der Kiwi ist ein Vogel, erbt also von Vogel, kann aber nicht fliegen. Die Methode fliegen()
wird aber trotzdem vererbt, was nicht korrekt ist.
Komposition:
|
|
oder mit Interface
|
|
Mit Komposition könnte man nun verschiedene Flugverhalten einfach austauschen, ohne die Vogelklasse zu ändern, was sie flexibler und wartbarer macht.
Zusammengefasst fördert das Prinzip “Komposition vor Vererbung” eine flexiblere und modularere Softwarearchitektur und hilft dabei, die Nachteile von starrer Vererbung zu vermeiden.
Kapselung
Kapselung ist eines der bedeutendsten Konzepte der objektorientierten Programmierung, welches Sicherheit bietet, indem es die sensiblen Daten/Implementierungsdetails einer Klasse vor den Benutzern verbirgt.
In Java kann die Kapselung erreicht werden, indem die Klassenattribute/-variablen als privat deklariert werden. Die Klasse stellt dann öffentliche Methoden zur Verfügung, welche von “aussen” ( durch andere Klassen) verwendet werden können, um bestimmte Information zu erhalten oder um bestimmten Operationen, welche auf den Attributen der Klasse basiert sind, ausführen zu können.
Beispiel
|
|
Die Student
Klasse beinhaltet vier private Attribute, worauf der Benutzer dieser Klasse keinen
Zugriff hat.
Die Klasse stellt neben dem Konstruktor lediglich zwei öffentliche Methoden zur Verfügung.
Die interne Struktur der Student
Klasse bleibt vom Benutzer verborgen.
So weiss der Benutzer z.B. nicht, dass die Liste der Courses mit einem Array umgesetzt worden ist.
Dies erlaubt eine Strukturänderung innerhalb der Student
Klasse, ohne dass der Benutzer
etwas davon merkt oder seinen Code ändern muss (die öffentlichen Methoden ändern sich nicht):
|
|
Die Student
Klasse erlaubt zusätzlich keinen direkten Zugriff auf ihre Attribute (es gibt keine
Getter-Methoden).
Die Überlegung hier ist, dass die einzelnen Attribute niemanden ausserhalb der Student
Klasse
interessieren.
Von aussen will man lediglich dem Student einen Kurs zuweisen können und alle Informationen zum
Studenten ausgeben.
Das Befolgen des Kapselung-Prinzips führt zu einem Design, welches folgende Vorteile mit sich bringt:
- Die Attribute und damit der Zustand einer Klasse bzw. eines Objektes sind vor “fremdem” Zugriff geschützt (Data-Hiding).
- Die Klasse hat eine öffentliche API, welche von Benutzern der Klasse verwendet werden kann. Somit ist auch klar definiert, was die Aufgabe dieser Klasse ist.
- Die öffentliche API einer Klasse ermöglicht das Verbergen von Umsetzungsdetails. Somit haben interne Strukturänderungen dieser Klasse keinen Einfluss auf den Code des Benutzers.
Jetzt bist du dran. Löse bitte die Aufgaben in den Labs.