Java ist eine objektorientierte Sprache. Das verlangt vom Entwickler neben dem Erlernen neuer Sprachelemente auch eine neue «objektorientierte» Denkweise. In Form eines Tutorials soll hier mit dieser Denkweise vertraut gemacht werden. Die folgenden Ausführungen sind dabei im wesentlichen eine Adaption des Tutorials Objektorientiertes Programmieren mit C++ auf die Programmiersprache Java.
Herkömmliche Softwareentwicklung bestand oftmals darin, zur Lösung eines vorgegebenen Problems Algorithmen zu entwerfen und diese in Prozeduren zu gießen, die in einer Programmiersprache - wie zum Beispiel C - formuliert sind. Man spricht daher auch von prozeduraler Programmierung.
Betrachtet man jedoch die reale Welt, so stellt man fest, dass die Dinge sich hier nicht in einer abstrakten prozeduralen Weise bewegen. Diesen Bruch zwischen realer Welt und Softwareentwicklung versucht der objektorientierte Ansatz zu überwinden. Analysiert man seine materielle Umgebung, so stellt man fest, dass diese im wesentlichen aus Objekten besteht, die in verschiedener Art und Weise miteinander agieren. So könnte es hier zum Beispiel gerade ein Objekt Fahrrad geben.
Nimmt man das Fahrrad noch weiter unter die Lupe, erkennt man, dass dieses Fahrrad bestimmte Eigenschaften bzw. Attribute (wie Farbe, Größe, momentane Geschwindigkeit) besitzt und dass es Methoden gibt, diese Eigenschaften zu verändern (z.B. die Methode "Tritt schneller", die zu einer Erhöhung der Geschwindigkeit führen wird oder die Methode "Bremse", die das Gegenteil bewirken sollte).
Transformiert man diese Erkenntnis aus der realen in die Software Welt, so kann man formulieren:
Ein Software Objekt ist ein Bündel aus Attributen und darauf bezogenen Methoden.
Wenn man verschiedene Fahrräder der realen Welt genauer betrachtet, so ist festzustellen, dass alle diese Fahrrad Objekte ähnlich sind: Alle besitzen Bremsen, alle haben irgendeine Farbe usw. Es muss also einen Bauplan geben, der beschreibt, wie ein Fahrrad grundsätzlich auszusehen hat. Alle Fahrrad Objekte sind nach diesem Bauplan erstellt worden - deswegen ist es auch möglich, ein beliebiges Fahrrad zu fahren, wenn man einmal das Fahrradfahren gelernt hat!
Eine Klasse ist ein Bauplan, welcher die Attribute und Methoden definiert, die alle Objekte einer bestimmten Art besitzen.
Die Abbildung zeigt eine Klasse StockItem in der sogenannten UML (unified modelling language) Notation. Diese Klasse könnte in einem Programm zur Aktienanalyse verwendet werden. Ein solches Programm muss viele verschiedene Aktien Objekte verwalten können. Damit dies effektiv geschehen kann, sollten alle diese verschiedenen Objekte jedoch nach einem einheitlichen Bauplan - der Klassendefinition - erstellt werden.
Der Name der Klasse steht in der UML-Notation im oberen Drittel des Rechtecks. Im mittleren Drittel stehen die Attribute. Die Beispielklasse StockItem definiert die zwei Attribute m_name und m_value, d.h. den Namen der Aktie und den momentanen Kurswert. Das Minuszeichen vor den Attributen bedeutet, das diese private Mitglieder der Klasse sind, d.h. von außen kann nicht direkt auf sie zugegriffen werden.
Im unteren Drittel stehen die Methoden der Klasse StockItem. Das Pluszeichen vor ihnen zeigt, dass sie public sind, d.h. die Methoden dürfen von anderen Objekten aufgerufen werden. Damit zeigt sich das Prinzip der Kapselung: Anstatt auf das Attribut m_value direkt zuzugreifen, müssen andere Objekte die Zugriffsmethoden setValue() und getValue() benutzen! So hat der Entwickler der Klasse die Möglichkeit, in setValue() noch zusätzliche Abfragen, z.B. bezüglich der Gültigkeit des Parameters, einzubauen.
Aufmerksamen Lesern wird bei Betrachtung der Abbildung nicht entgangen sein, dass Methoden in Java - wie auch in C++ - nicht nur durch den Namen, sondern durch Namen und Parameteranzahl und -typ unterschieden werden! So kann es die Methode setValue() zweimal geben: Einmal mit einem Parameter vom Typ double und mit einem vom Typ String. Der Compiler erkennt beim Aufruf dieser Methode anhand des Parametertyps selbsttätig, welche Variante er verwenden muss.
In Java gibt es keine Trennung zwischen Definition und Implementierung einer Klasse. Beides erfolgt in einer Textdatei mit der Extension .java, die den Namen der Klasse trägt. Eine Unterteilung in Header- und Implementierungsdatei wie bei C++ gibt es nicht.
Die Definition und Implementierung der Java Klasse für StockItem wird also in der Datei mit dem Namen StockItem.java vorgenommen:
package de.kompf.tutor; /** * Class representing a StockItem. */ public class StockItem { private String m_name; private double m_value; /** * Construct a new StockItem with empty name and no value. */ public StockItem() { this("", 0.0); } /** * Construct a new StockItem with no value. * * @param name The name of the StockItem. */ public StockItem(String name) { this(name, 0.0); } /** * Construct a new StockItem. * * @param name The name. * @param value The value. */ public StockItem(String name, double value) { m_name = name; m_value = value; } /** * @return The name. */ public String getName() { return m_name; } /** * @param val The value to set. */ public void setValue(double val) { m_value = val; } /** * @param val The value to set. */ public void setValue(String val) { m_value = Double.parseDouble(val); } /** * @return The value. */ public double getValue() { return m_value; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return m_name + ": " + m_value; } }
Die Datei beginnt mit einer Package Deklaration. Dadurch lassen sich auch große Projekte gut strukturieren. Zum einen dient der Packagename de.kompf.tutor als eine Art Namensraum: Klassennamen müssen nur innerhalb einer Package eindeutig sein. Zum anderen werden die Dateien entsprechend ihres Packagenamens auf der Platte abgelegt: Alle zur Package de.kompf.tutor gehörenden Java Dateien befindet sich im Verzeichnis de/kompf/tutor relativ zum Projektverzeichnis.
Die eigentliche Klassendefinition beginnt mit den Schlüsselwörtern public class gefolgt vom Namen der Klasse. Der Klassenrumpf wird von geschweiften Klammern { } umschlossen. Er enthält sämtliche Methoden und Attribute («Member») der Klasse. Dabei kennzeichnen die Schlüsselwörter public und private vor jeder Deklaration die im letzten Abschnitt besprochene öffentliche oder private Sichtbarkeit des Members. Es gibt außerdem noch die Möglichkeit, mittels protected Variablen und Methoden zu deklarieren, die ausschließlich abgeleiteten Klassen zur Verfügung stehen sollen. Dazu mehr später mehr beim Thema Vererbung. Es ist auch möglich, die Sichtbarkeit nicht explizit anzugeben. Dann ist der entsprechende Member in der zugehörigen Package sichtbar.
Jede Klasse benötigt mindestens einen Konstruktor (kurz ctor) zur Erzeugung von Objekten der Klasse. Ein Konstruktor hat den gleichen Namen wie die Klasse und keinen Rückgabewert. Eine Angabe der Sichtbarkeit per public, private und so weiter ist auch für Konstruktoren möglich. Man kann auch auf die explizite Angabe eines Konstruktors verzichten, in diesem Fall hat die Klasse automatisch einen impliziten public Konstruktor ohne Parameter, den Default Konstruktor.
Die Klasse besitzt drei Konstruktoren, um eine Initialisierung mit verschiedenen Kombinationen von Parametern zu erlauben. Eine Angabe von Defaultwerten direkt in der Parameterliste analog zu C++ ist in Java nicht möglich. Die restliche Implementierung besteht aus den Methoden zum Setzen und Auslesen der Membervariablen. Eine Methode zum Setzen des Namens fehlt mit Absicht, es soll nicht möglich sein, den Namen eines StockItems im Nachhinein zu ändern.
Im Gegensatz zur gleichnamigen C++ Klasse aus dem C++ Tutorial ist es in Java nicht notwendig, sich mit Dingen wie Copy-Constructor, Destructor und Assignment Operator herumzuschlagen. Warum dies so ist, wird bei der Betrachtung des Objektlebenszyklus verständlich.
Der Lebenszyklus eines Java Objektes besteht aus den Abschnitten
Das Erzeugen eines Objektes erfolgt immer dynamisch mittels des new Operators in Verbindung mit einem Konstruktor. Das Ergebnis kann einer Variablen mit passendem Typ zugewiesen werden:
StockItem bas = new StockItem("BAS", 120.34);
Eine statische Erzeugung ohne new analog zu C++ ist in Java nicht möglich. Eine Objektvariable enthält demnach immer eine Referenz auf das Objekt und nicht das Objekt selbst.
Ein Objekt wird benutzt, indem eine seiner Methoden aufgerufen wird:
bas.setValue(52.80); double v = bas.getValue(); // v sollte jetzt den Wert 52.8 haben
Außerdem kann ein Objekt als Parameter an eine Methode übergeben oder von dieser per return zurückgeliefert werden. Die Parameterübergabe an Methoden erfolgt in Java immer per Value. Da die Objektvariable in Java aber nur die Referenz auf das Objekt und nicht das Objekt selbst enthält, erfolgt hierbei kein Kopieren des Objektes. Als Konsequenz sind Änderungen am Objekt, die eine Methode vornimmt, auch im aufrufenden Kode sichtbar:
class Converter { public static void split(StockItem item, double count) { item.setValue(item.getValue() / count); } } bas.setValue(52.80); Converter.split(bas, 2); System.out.println(bas); // gibt 'BAS: 26.4' aus
Die Zuweisung eines Objektes an eine Variable betrifft dann ebenso nur die Objektreferenz. Es gibt in Java kein Pendant zum Assignment Operator aus C++.
StockItem x = new StockItem("XYZ", 42); StockItem y = new StockItem("ABC", 22); x = y; // Die Variable x enthält eine Referenz auf das Objekt mit dem Namen "ABC". // Das Objekt namens "XYZ" wird nicht mehr referenziert und irgendwann vom // Garbage Collector entsorgt.
Zuweisungen und Parameterübergaben betreffen in Java also immer nur die Objektreferenz. Als Konsequenz wird niemals eine automatische Kopie von Objekten erzeugt. Benötigt man aber doch einmal eine Kopie eines Objektes, kann man dafür seine clone() Methode verwenden. Diese Methode muss vom Entwickler kodiert werden, es gibt hierfür keinen Automatismus. Der Fall, das man eine Kopie eines Objektes benötigt, tritt allerdings äußerst selten auf. Dieses Einsteigertutorial verzichtet daher auf eine nähere Betrachtung von clone().
Bei der im vorigen Abschnitt eingeführten Methode split handelt es sich um eine statische Methode. Im Programmcode werden statische Methoden durch das Keyword static bei der Methodendeklaration gekennzeichnet.
Statische Methoden gehören zu einer bestimmtem Klasse und nicht zu einem Objekt. Daher steht beim Aufruf einer solchen Methode vor dem Punkt auch ein Klassenname und keine Variable. Statische Methoden haben logischerweise keinen Zugriff auf die nicht-statischen Variablen und Methoden der Klasse. Oft werden static Methoden als Utility- und Hilfsfunktionen verwendet, die nicht im Kontext eines bestimmten Objektes ausgeführt werden müssen. Prominentes Beispiel dafür sind die mathematischen Funktionen aus java.lang.Math.
Der Java Programmierer braucht sich nicht um das Aufräumen nicht mehr benötigter Objekte zu kümmern. Das erledigt der Garbage Collector (GC) für ihn.
Der GC führt Buch über alle per new angelegten Objekte. Er prüft in regelmäßigen Abständen, ob es Objekte gibt, die von keiner Variablem mehr referenziert werden. Der von diesen Objekten belegte Speicherplatz wird dann freigegeben.
Es gibt in Java im Unterschied zu C++ daher auch keinen delete Operator und keinen Destruktor. Falls man doch eine Möglichkeit benötigt, kurz vor dem Entfernen des Objektes noch eigenen Programmcode auszuführen, kann dafür die Methode finalize() genutzt werden.
Vererbung erlaubt die Definition neuer Klassen auf der Basis von bestehenden Klassen. Dies ist ein grundlegendes Konzept objektorientierten Designs. Weiter oben wurde der Begriff der Klasse als eine Art Bauplan für Objekte erklärt. Anhand des dort verwendeten Beispiels "Fahrrad" lassen sich weitere Parallelen zur realen Welt ziehen: Es fällt auf, dass es hier verschiedene Arten von Fahrrädern gibt: Rennräder, Montainbikes, Trekkingräder und das gute alte Hollandrad. Warum sind alle diese verschiedenen Räder als Fährräder erkennbar? Weil sie gewisse gemeinsame Eigenschaften haben: Alle haben zwei Räder, einen Lenker und lassen sich durch Tritt auf die Pedale fortbewegen. Zusätzlich zu diesen Gemeinsamkeiten bringen sie aber auch neue Eigenschaften ein: Montainbikes und Rennräder haben jeweils eine Gangschaltung, unterscheiden sich aber durch die Art der Bereifung.
In objektorientierter Sprache könnte man also sagen: Die Klassen Montainbikes, Rennräder und Hollandräder erben von der Klasse Fahrräder gemeinsame Eigenschaften und fügen zusätzliche hinzu. Allgemein gilt:
Es ist wichtig zu verstehen, dass Vererbung nur in eine Richtung läuft: Ein Rennrad ist zwar immer auch ein Fahrrad, aber nicht jedes Fahrrad ist automatisch ein Rennrad.
Die bereits definierte und benutzte Klasse StockItem erlaubt die Speicherung eines Namens und eines dazugehörenden Wertes und ist zur Darstellung von Aktienkursen gedacht. Wer sich mit dieser Materie schon beschäftigt hat, der weiß, dass zu einer Aktie noch viele Informationen mehr gespeichert werden können. So gibt es neben dem (Tages- oder Wochen-)Schlusskurs (Close) noch Eröffnungs- (Open), Höchst- (High) und Tiefstkurs (Low). StockItem soll daher um die Möglichkeit erweitert werden, statt nur einem Wert die Eröffnungs- und Schlusskurse abfragen und setzen zu können.
Diese Erweiterung soll jedoch nicht durch Verändern der existierenden Klasse StockItem erfolgen - diese Klasse wird bereits in vielen Softwareprojekten benutzt und eine Änderung ihrer Funktionalität könnte unter Umständen böse Auswirkungen haben. Auch will man das Rad nicht völlig neu erfinden - vorhandener Code soll so weit wie möglich wiederverwendet werden. Das alles lässt sich dadurch erreichen, indem eine neue Klasse StockItemOC definiert wird, die von der vorhandenen Klasse StockItem abgeleitet ist:
package de.kompf.tutor; /** * Class representing a StockItem with open and close value. */ public class StockItemOC extends StockItem { private double m_open; private double m_close; /** * Construct a new StockItemOC with empty name and no values. */ public StockItemOC() { } /** * Construct a new StockItemOC with no values. * * @param name The name. */ public StockItemOC(String name) { super(name); } /** * Construct a new StockItemOC. * * @param name The name. * @param open The open value. * @param close The close value. */ public StockItemOC(String name, double open, double close) { super(name); m_open = open; m_close = close; } /** * @return The value of this item which is identical to the close value. */ @Override public double getValue() { return m_close; } /** * @param val The value of this item to set. */ @Override public void setValue(double val) { m_close = val; } /** * @return The open value. */ public double getOpen() { return m_open; } /** * @param open The open value to set. */ public void setOpen(double open) { m_open = open; } /** * @return The close value. */ public double getClose() { return m_close; } /** * @param close The close value to set. */ public void setClose(double close) { m_close = close; } /** * @see Object#toString() */ @Override public String toString() { return getName() + ": " + m_open + "/" + m_close; } }
Die einzige syntaktische Erweiterung gegenüber der bekannten Klassendefinition ist, dass zu Beginn der Definition nach dem Klassennamen StockItemOC das Schlüsselwort extends gefolgt vom Namen der Basisklasse angegeben wird.
Welche Auswirkung hat diese Vererbungsbeziehung nun auf das Verhalten der abgeleiteten Klasse StockItemOC?
Interessant sind auch die Konstruktoren der Klasse: Da ja kein Zugriff auf die private Membervariablen der Basisklasse erlaubt ist, erfolgt das Setzen von m_name über den Aufruf des Konstruktors der Basisklasse per super(Parameterliste). Der Aufruf von super muss immer das erste Statement im Konstruktor sein. Lässt man ihn - wie im parameterlosen Defaultkonstruktor des Beispiels - weg, dann fügt der Compiler implizit super() ein.
Die Verwendung von abgeleiteten Klassen soll nun anhand der Klasse StockItemOC in einem kleinen Testprogramm demonstriert werden.
Als Eintrittspunkt in ein Java-(Konsolen-)Programm dient die spezielle statische Methode main mit genau der im folgenden Beispiel angegebenen Signatur:
public static void main(String[] args) { StockItem a = new StockItem("BAY", 34.9); StockItem b = new StockItem("BAS", 24.2); StockItemOC c = new StockItemOC("DTE", 57.0, 59.4); System.out.println(a.getName() + ": " + a.getValue()); System.out.println(b.getName() + ": " + b.getValue()); System.out.println(c.getName() + ": " + c.getValue() + " (" + c.getOpen() + "/" + c.getClose() + ")");
Dies bietet auf den ersten Blick nichts neues. Beim zweiten Hinsehen erkennen wir, dass in der letzten Anweisung die Methode getName() des Objektes c, welches vom Typ StockItemOC ist, aufgerufen wird. Die Klasse StockItemOC hat aber gar keine Methode getName() definiert! Mit dem neu erworbenen Wissen über Vererbung ist jedoch klar, was passiert: Es wird einfach die von der Klasse StockItem geerbte Methode verwendet!
Interessant ist auch die Verwendung der Methode getValue(). Diese gibt es sowohl in StockItem als auch in StockItemOC. In diesem Beispiel ist jedoch relativ einfach zu verstehen, was passiert: In der letzten Anweisung ruft der Java Compiler StockItemOC.getValue() auf, in den Anweisungen davor jedoch StockItem.getValue(). Das ist eindeutig, weil der Typ der Objekte a, b und c bereits zur Übersetzungszeit bekannt ist.
Was passiert jedoch, wenn der Compiler beim Übersetzen den genauen Typ des Objektes noch nicht kennt? Hier ist die Fortsetzung des Programms:
// array with element type StockItem StockItem[] astocks = new StockItem[3]; astocks[0] = a; astocks[1] = b; astocks[2] = c; for (int i = 0; i < astocks.length; ++i) { System.out.println(astocks[i].getName() + ": " + astocks[i].getValue()); }
Beim Aufruf astocks[i].getValue() muss je nachdem, ob die in astocks stehende Variable ein Objekt vom Typ StockItem (bei Index 0 und 1) oder vom Typ StockItemOC (Index 2) referenziert, eine andere Methode aufgerufen werden. Dies kann erst zur Laufzeit entschieden werden, man spricht daher von dynamischer oder später Bindung.
Der Java Compiler arbeitet immer mit dynamischer Bindung. C++ dagegen erlaubt hier die Steuerung der Bindungsart (statisch oder dynamisch) per Schlüsselwort virtual - diese Falle gibt es in Java zum Glück nicht.
Zum Schluss noch eine fortgeschrittene Anwendung. Statt in ein Array werden die Objekte in eine Liste gepackt. Anschließend werden sie nach ihrem Wert (getValue()) sortiert und ausgegeben. Das alles funktioniert auch dann, wenn die Liste sowohl Objekte vom Typ StockItem als auch StockItemOC enthält:
// get the stock items ordered by price List<StockItem> stocks = new LinkedList<StockItem>(); stocks.add(a); stocks.add(b); stocks.add(c); Collections.sort(stocks, new Comparator<StockItem>() { public int compare(StockItem s1, StockItem s2) { return (int) Math.signum(s1.getValue() - s2.getValue()); } }); System.out.println("stock items ordered by price"); System.out.println(stocks); }
Für die Sortierung ist ein Comparator notwendig. Dessen Methode compare vergleicht die Werte zweier StockItems. Wie man sieht, reicht es völlig aus, compare für Parameter des Typs StockItem zu implementieren. Es ist beim Aufruf der Methode dann egal, ob der aktuelle Parameter auf ein Objekt vom Typ StockItem oder StockItemOC zeigt. Dies ist ein Resultat (und Vorteil) der verwendeten Vererbungshierarchie.
Die Ausgabe aller Items erfolgt ganz einfach per Übergabe der kompletten Liste an System.out.println, was im Beispiel den Text
[BAS: 24.2, BAY: 34.9, DTE: 57.0/59.4]
in die Standardausgabe schreibt.
System.out.println verlangt als Parameter eine Variable vom Typ Object. Im Beispiel wird jedoch eine List übergeben. Bei der Erklärung, warum das funktioniert, kommt man zu einer wichtigen Eigenschaft aller Klassen in Java:
Alle Java Klassen sind implizit von der Klasse java.lang.Object abgeleitet.
Also auch die Klassen StockItem und StockItemOC! System.out.println ruft nun die Methode toString() des übergebenen Objekts auf und schreibt den Rückgabewert in die Standardausgabe. Die Klasse LinkedList (die ja auch von Object erbt) implementiert toString() in der Form, dass für jedes Element der Liste ebenfalls toString() aufgerufen wird. Damit wird jetzt klar, warum in den Klassendefinitionen für StockItem und StockItemOC (siehe oben) jeweils die Methode toString() implementiert ist! Hätte man dies nicht getan, dann sähe die Ausgabe etwa so aus:
[de.kompf.tutor.StockItem@11b86e7, de.kompf.tutor.StockItem@35ce36, de.kompf.tutor.StockItemOC@757aef]
Es käme dann die von Object geerbte toString() Methode zur Anwendung, die den Klassennamen und eine Adresse ausgibt.
Bei einigen Vererbungshierarchien kommt man vielleicht zu der Erkenntnis, dass für bestimmte Basisklassen nie Objekte instanziert werden. Im oben eingeführten Fahrradbeispiel würde das dann heißen, dass es keine Fahrräder vom Typ «Fahrrad» gibt, sondern jedes Fahrad sich einem speziellen, abgeleiteten Typ, wie Rennrad, Hollandrad und so weiter zuordnen lässt. Die Basisklasse Fahrrad wird dann als abstrakt bezeichnet.
Wenn eine abstrakte Klasse nur noch Methodensignaturen, aber keine Implementierung mehr enthält, wird sie zum interface. Java verwendet dafür das reservierte Schüsselwort interface.
Ein interface verwendet man, um das Verhalten von Objekten festzulegen, bei denen man sich (noch) nicht auf eine bestimmte Implementierung festlegen will. Zum Beispiel könnte man so einen QuoteService definieren, der die Abfrage von Aktienkursen erlaubt:
package de.kompf.tutor; /** * Interface to define a service to get quotes. */ public interface QuoteService { /** * Get the last known price for a stock item. * * @param name The name of the item. * @return The price. */ public double lastPrice(String name); /** * Get a stock item with the open and close at a given date. * * @param name The name of the item. * @param date The date. * @return The StockItemOC. */ public StockItemOC openClose(String name, java.util.Date date); }
Zu diesem Zeitpunkt gibt es noch keine Implementierung des Interface. Man kann beim Programmieren aber schon so tun, als gäbe es eine, indem einfach gegen das Interface programmiert wird:
List<StockItem> myStocks; QuoteService quoteService; // ... // get actual quotes for my stock for (StockItem stockItem : myStocks) { double lastPrice = quoteService.lastPrice(stockItem.getName()); stockItem.setValue(lastPrice); }
Um das Programm jetzt schon zu testen, kann man sich einen «Mock» Service schreiben, der das Interface QuoteService implementiert und irgendwelche Werte zurückliefert:
public class MockQuoteService implements QuoteService { public double lastPrice(String name) { return 10.0; } public StockItemOC openClose(String name, Date date) { return new StockItemOC(name, 10.0, 11.0); } }
Im Code ergänzt man dann
quoteService = new MockQuoteService();
Später, wenn die richtige Implementierung, zum Beispiel als Klasse YahooQuoteService zur Verfügung steht, ändert man diese Zeile einfach in
quoteService = new YahooQuoteService();
und benutzt von diesem Zeitpunkt an eine andere Implementierung. Am restlichen Programm dürfte sich nichts mehr ändern, wenn man konsequent gegen das Interface programmiert hat.
Diese Möglichkeit zur Trennung von Interface und Implementierung ist ein großer Vorteil von Java. Insbesondere unter den Aspekten der Teamarbeit und der Wiederverwendbarkeit von Programmcode sollte man sich die Regel «Programmiere gegen Interfaces statt gegen Implementierungen» verinnerlichen.