11.2 Werkzeuge
In diesem Abschnitt werden einige wichtige Hilfsmittel für die Softwareentwicklung vorgestellt:
- Die Diagrammspezifikation UML (Unified Modeling Language) bietet verschiedene standardisierte Diagrammtypen zur Darstellung diverser Projektbestandteile in den verschiedenen Phasen des Entwicklungsprozesses.
- Entwurfsmuster katalogisieren erfolgreiche Lösungsmodelle für die einfache Übernahme in späteren Projekten.
- Unit-Tests ermöglichen den automatisierten Test der Funktionalität einer Klasse.
Diese Hilfsmittel können in vielen unterschiedlichen Entwicklungsmodellen zum Einsatz kommen, obwohl einige von ihnen im Zusammenhang mit konkreten Verfahren entwickelt wurden.
11.2.1 UML
Der Begriff Sprache für die Unified Modeling Language (UML) ist ein wenig irreführend: Zwar handelt es sich um eine bestimmte Ausdrucksweise, aber ihr Vokabular sind keine Wörter, sondern verschiedene Arten von Diagrammen. Die UML ist gut dazu geeignet, die diversen Aspekte der Softwareentwicklung übersichtlich darzustellen. Ihr Arbeitsschwerpunkt sind die Analyse- und die Entwurfsphase.
Die UML wurde in den 90er-Jahren von den »drei Amigos« Grady Booch, Ivar Jacobson und James Rumbaugh durch Zusammenführung einiger früherer Ansätze entwickelt. Zunächst wurde sie vor allem von dem Entwicklungstool-Hersteller Rational gefördert und weiterentwickelt. Später wurde sie von der Object Management Group (OMG) standardisiert und fand allgemeine Verbreitung. Ende 2003 wurde die neue Version 2.0 veröffentlicht, die einige Erweiterungen und Verbesserungen bietet.
Die wichtigsten Diagrammtypen der UML sind die Folgenden:
- Anwendungsfalldiagramme (Use Case Diagrams) stellen die Anforderungen der Benutzer dar. Sie prägen das Bild der UML in der Öffentlichkeit, weil sie die typischen »Strichmännchen« (Akteure) definieren.
- Klassendiagramme (Class Diagrams) verdeutlichen die Klassenstruktur und damit die Grundarchitektur des Systems.
- In Kollaborationsdiagrammen (Collaboration Diagrams) wird das Zusammenwirken verschiedener Objekte dargestellt.
- Sequenzdiagramme (Sequence Diagrams) stellen besonders den zeitlichen Ablauf dieser Zusammenarbeit dar.
- In Zustandsdiagrammen (State Diagrams) werden die verschiedenen Zustände eines Objekts und die Übergänge zwischen diesen Zuständen dargestellt.
- Pakete (Packages) dienen der Verdeutlichung hierarchischer Objektstrukturen.
- Aktivitätsdiagramme (Activity Diagrams) stellen den logischen und zeitlichen Ablauf verschiedener Aktivitäten dar.
Es ist nicht Sinn der Sache, UML-Diagramme auf Dauer mit dem Stift auf Papier zu malen (außer für erste Ideensammlungen oder bei Projektbesprechungen). Sie sollten die Diagramme auch nicht mühevoll in einem allgemeinen Grafikprogramm wie Illustrator zeichnen. Viel verbreiteter und erheblich produktiver sind UML-Tools, mit denen sich die Diagramme einfach, schnell und übersichtlich erstellen lassen. Solche Programme werden auch als CASE-Tools (Computer-Aided Software Engineering) bezeichnet. Es gibt sowohl Open-Source-Lösungen als auch sehr teure kommerzielle Produkte.
Ein praktisches Open-Source-Tool ist ArgoUML (http://argouml.tigris.org). Es ist in Java geschrieben und läuft somit auf den meisten wichtigen Systemen. Eine Installation ist nicht erforderlich. Sie benötigen lediglich eine funktionierende Java-Installation (siehe Kapitel 9, »Grundlagen der Programmierung«). Laden Sie die Archivdatei herunter, entpacken Sie sie in ein beliebiges neues Verzeichnis, und geben Sie Folgendes ein, um das Programm zu starten:
$ java -jar argouml.jar
Alternativ lässt sich das Tool in neueren Browsern auch durch einen Java Web Start-Link auf der Website direkt starten.
Abbildung 11.3 zeigt das Programm beim Erstellen eines Klassendiagramms. Die obere der beiden Symbolleisten enthält Schaltflächen für die verschiedenen Diagrammtypen, die untere bietet Werkzeuge für die einzelnen Bestandteile des aktuellen Diagramms. Nachdem Sie einen dieser Buttons angeklickt haben, können Sie das Element mit einem weiteren Klick auf der Arbeitsfläche platzieren und im unteren Feld seine Eigenschaften einstellen. Aus dem jeweils ausgewählten Element lassen sich automatisch Verbindungslinien herausziehen, um es mit anderen Elementen zu verknüpfen.
Im Folgenden werden vier UML-Diagrammarten näher vorgestellt: Anwendungsfalldiagramme, Klassendiagramme, Sequenzdiagramme und Aktivitätsdiagramme.
Abbildung 11.3 ArgoUML beim Erstellen eines Klassendiagramms
Anwendungsfalldiagramme
In einem Anwendungsfalldiagramm werden Anwendungsfälle (Use Cases) aus der Sicht der Beteiligten, der sogenannten Akteure, dargestellt. Dieser Diagrammtyp ist vornehmlich ein Bestandteil der Analyse. Abbildung 11.4 zeigt ein Beispiel mit zwei Akteuren, einem Kunden und einem Verkäufer. Die drei möglichen Geschäftsvorfälle, die zwischen ihnen stattfinden können, sind Information, Verhandlung und Kauf.
Die <<include>>-Beziehung zwischen Kauf und Verhandlung bedeutet, dass der Verkauf eine Verhandlung umfassen kann; ebenso verhält es sich mit der Beziehung zwischen Verhandlung und Information. Andere mögliche Beziehungen sind:
- Eine durchgehende Linie mit einer hohlen Pfeilspitze steht für eine Generalisierung; der Anwendungsfall am Pfeilanfang ist eine Spezialisierung desjenigen am Pfeilende.
- Ein gestrichelter Pfeil mit der Beschriftung <<extend>> besagt, dass der Anwendungsfall am Pfeilursprung denjenigen, auf den gezeigt wird, erweitert.
Bei der späteren Implementierung können sich aus diesen Beziehungen Vererbungsverhältnisse ergeben.
Klassendiagramme
Klassendiagramme stellen die Elemente einer Klasse sowie die Beziehungen zwischen verschiedenen Klassen dar. Jede Klasse wird durch ein Rechteck dargestellt; es gibt drei Detailstufen:
- nur Klassenname
- Klassenname und Attribute (Eigenschaften)
- Klassenname, Attribute und Methoden
Die (bis zu) drei Kategorien werden durch waagerechte Linien voneinander getrennt. Abstrakte Klassen (ohne Methodenimplementierung) werden durch <<abstrakt>> unter dem Klassennamen gekennzeichnet, Interfaces durch <<interface>>.
Die Vererbung wird durch einen Pfeil mit leerer Spitze dargestellt, der von der abgeleiteten auf die übergeordnete Klasse zeigt. In Abbildung 11.5 sehen Sie die Beziehungen zwischen der Oberklasse Artikel und den abgeleiteten Klassen Buch und DVD, jeweils mit allen drei Informationen. Die zusätzliche Angabe der Datentypen von Methoden und Attributen ermöglicht die automatische Erzeugung des Code-Grundgerüsts für die Klassen. In ArgoUML erfolgt dies beispielsweise über Generieren · Alle Klassen generieren, sofern gerade ein Klassendiagramm angezeigt wird.
Abbildung 11.5 Ein UML-Klassendiagramm mit einer Elternklasse und zwei abgeleiteten Klassen
Neben den Vererbungslinien gibt es auch komplexere Beziehungen zwischen Klassen beziehungsweise Instanzen. Allgemein spricht man bei der Vererbung von »IS A«-Beziehungen, denn eine speziellere Klasse »ist« auch immer die Elternklasse. Im vorliegenden Beispiel gilt etwa, dass ein Buch unter anderem ein Artikel ist. Enthält eine Klasse Attribute, die Instanzen einer anderen Klasse sind, spricht man dagegen von einer »HAS A«-Beziehung – ein Objekt »hat« oder enthält ein anderes.
In der UML werden »HAS A«-Beziehungen durch eine Verbindungslinie zwischen den beiden Klassen dargestellt. Auf der Seite der Klasse, die die andere enthält, wird die Linie durch eine Raute gekennzeichnet. Hier gibt es zwei Möglichkeiten:
- Aggregation: Die enthaltenen Elemente existieren unabhängig von der enthaltenden Klasse; dies wird durch eine leere Raute gekennzeichnet.
- Komposition: Die enthaltenen Elemente existieren nur in Abhängigkeit von der enthaltenden Klasse, sind also eher abstrakte Eigenschaften als konkrete Gegenstände; diese Beziehung wird durch eine gefüllte Raute dargestellt.
Die Kardinalität (Häufigkeit) der beteiligten Elemente kann durch Zahlen an den Verbindungsstellen dargestellt werden:
- Eine einfache Zahl (zum Beispiel 1) bedeutet, dass genauso viele Elemente dieses Typs beteiligt sind. Beispielsweise könnte man die Beziehung zwischen einem Pkw und seinen Rädern durch die festen Zahlen 1 beziehungsweise 4 kennzeichnen.
- Ein Sternchen (*) steht für beliebig viele, also kein Element, ein Element oder mehrere Elemente.
- Ein Zahlenbereich (m..n) bedeutet mindestens m und höchstens n Elemente. Die Beziehung zwischen einem allgemeinen Fahrzeug und seinen Rädern ist beispielsweise 1 zu 1..* (von der Schubkarre bis zum beliebig langen Güterzug).
Neben den »HAS A«-Beziehungen gibt es auch diverse lockere Verbindungen zwischen Klassen, genannt Assoziationen. Sie werden durch eine Verbindungslinie dargestellt. Eine offene Pfeilspitze oder ein daneben gezeichnetes Richtungsdreieck sowie eine Textbezeichnung beschreiben die Beziehung näher. Auch hier können die Kardinalitätsbezeichnungen verwendet werden.
In Abbildung 11.6 finden Sie zwei Beispiele für solche Klassenbeziehungen: Ein Supermarkt kann eine oder mehrere Kassen enthalten; da diese unabhängig vom Supermarkt existieren, bleibt die Raute leer (Aggregation). Die zweite Beziehung zeigt, dass ein Kassierer beliebig viele Artikel registrieren kann. Die Kardinalität 1 bei Supermarkt beziehungsweise Kassierer wurde als »Standardfall« einfach weggelassen; dies ist eine (zulässige) Eigenart von ArgoUML.
Abbildung 11.6 Klassenbeziehungen in UML-Klassendiagrammen: Aggregation (oben) und Assoziation (unten)
Sequenzdiagramme
In einem Sequenzdiagramm wird der zeitliche Ablauf einer Anwendung als Abfolge von Nachrichten zwischen den Objekten dargestellt. Die beteiligten Objekte werden waagerecht auf »Swim Lanes« (Schwimmbahnen) nebeneinander platziert. Der Arbeitsablauf wird von oben nach unten dargestellt. Auf jeder Bahn wird eine gestrichelte Linie gezeichnet, solange ein Objekt existiert, oder ein schmales Rechteck, wenn das Objekt gerade den Programmfluss kontrolliert. Methodenaufrufe werden mithilfe von durchgezogenen Pfeilen gekennzeichnet, Nachrichten durch gestrichelte Pfeile. Abbildung 11.7 zeigt das einfache Beispiel eines Einkaufsvorgangs.
Abbildung 11.7 UML-Sequenzdiagramm eines Einkaufsvorgangs
Aktivitätsdiagramme
Aktivitätsdiagramme lassen sich als Weiterentwicklung der klassischen Flussdiagramme betrachten. Sie dienen der Darstellung des Zusammenspiels verschiedener Aktivitäten, die durch seitlich abgerundete Kästen dargestellt werden. Andere wichtige Symbole sind diese:
- Ein dicker, gefüllter Kreis markiert den Startzustand.
- Ein hohler Kreis, der einen kleineren, gefüllten enthält, kennzeichnet den Endzustand, der alle Aktivitäten abschließt.
- Fallentscheidungen werden durch eine Raute mit mehreren abgehenden Pfeilen dargestellt.
- Eine dicke horizontale Linie dient entweder der Aufteilung (Forking) eines Ablaufs in mehrere parallele Verarbeitungsstränge oder der Zusammenführung zuvor verzweigter Abläufe.
In Abbildung 11.8 werden mögliche Abläufe des Anwendungsfalls »Information« aus dem Anwendungsfalldiagramm in Abbildung 11.3 verdeutlicht. In ArgoUML können Sie ein Aktivitätsdiagramm eines Anwendungsfalls erstellen, sobald dieser markiert ist.
Abbildung 11.8 UML-Aktivitätsdiagramm des Anwendungsfalls »Information«
11.2.2 Entwurfsmuster
Entwurfsmuster (Design Patterns) sind ursprünglich ein Konzept aus der (Gebäude-) Architektur, das im Rahmen der Programmiersprache und -umgebung Smalltalk für die Softwareentwicklung übernommen wurde. Im Wesentlichen geht es um die übersichtliche Katalogisierung einmal gefundener Lösungen für die spätere Wiederverwendung. Beachten Sie, dass Entwurfsmuster keine fertig programmierten Komponenten oder Codeschnipsel sind. Wie der Name schon sagt, gehören sie zur Phase des Entwurfs und nicht zur Implementierung von Software. Dennoch enthält ein Muster neben vielen anderen Komponenten auch Codebeispiele.
In der Softwareentwicklung wurden die Entwurfsmuster durch die »Gang of Four« (GoF) Erich Gamma (der ehemalige Eclipse-Entwicklungsleiter), Richard Helm, Ralph Johnson und John Vlissides eingeführt. Ihr Buch »Design Patterns« (siehe Anhang C) ist die wichtigste Informationsquelle und gewissermaßen der ursprüngliche Hauptkatalog für Entwurfsmuster. Daneben wurden zahlreiche weitere Musterkataloge entwickelt, beispielsweise sogenannte Enterprise Design Patterns, die auch Geschäftsvorfälle einbeziehen.
Ein bekanntes Entwurfsmuster, das nicht im GoF-Katalog vorkommt, ist zum Beispiel das MVC-Pattern (Model, View, Controller). Es handelt sich um eine praktische Vorgehensweise zur sauberen Trennung von Datenmodell, Programmlogik und Präsentation. Es wurde bereits in den 70er-Jahren im Smalltalk-Umfeld entwickelt und beschreibt den Idealzustand von APIs für grafische Benutzeroberflächen. Inzwischen wird es aber auch für Web-Frameworks wie Ruby on Rails (siehe Kapitel 18, »Webserveranwendungen«) genutzt.
Schema für Entwurfsmuster
Jedes Entwurfsmuster besteht aus vier wesentlichen Komponenten:
- Name: Das Muster sollte eine möglichst griffige Bezeichnung erhalten, die möglichst genau auf seinen Verwendungszweck hindeutet.
- Problem: eine genaue Beschreibung der Situation, in der das Entwurfsmuster eingesetzt werden kann
- Lösung: die abstrakte Beschreibung eines Entwurfs, der das Problem löst
- Konsequenzen: eine Beschreibung der Folgen und möglichen Nebeneffekte, die der Einsatz des Patterns mit sich bringt
Pattern-Kataloge wie derjenige in dem zuvor genannten Buch »Design Patterns« verwenden allerdings eine erheblich genauere Struktur zur Beschreibung jedes Musters. Es handelt sich um eine Auflistung der folgenden Punkte (in Klammern jeweils die Originalbezeichnungen aus dem GoF-Buch):
- Name und Einordnung (Pattern Name and Classification): Die Bedeutung eines sprechenden Namens braucht unter Programmierern nicht weiter betont zu werden. Die Einordnung beschreibt das Einsatzgebiet (Purpose) und den Geltungsbereich (Scope) des Musters. Man unterscheidet drei grundlegende Einsatzgebiete: Erzeugungsmuster (Creational Patterns) sind Lösungen für verschiedene Probleme der Objekterzeugung; Strukturmuster (Structural Patterns) beschäftigen sich mit Problemstellungen der Datenstruktur, und Verhaltensmuster (Behavioral Patterns) beschreiben die Implementierung häufig benötigter Verhaltensweisen von Objekten. Der Geltungsbereich ist »Klasse« für statische, durch Vererbung angewendete Muster oder »Objekt« für Muster, die Objektbeziehungen betreffen. Letztere kommen wesentlich häufiger vor.
- Absicht (Intent); kurze Beschreibung der Aufgabe des Entwurfsmusters und mögliche Gründe für seinen Einsatz
- Alias (Also Known As): Viele Muster sind unter verschiedenen Namen bekannt; andere gängige Bezeichnungen werden hier aufgelistet.
- Motivation: ein konkretes Beispielszenario, das den Einsatzzweck des Musters deutlich macht
- Verwendungszweck (Applicability): Beschreibung der Situationen, in denen das Pattern eingesetzt werden kann, und der Probleme, die es lösen hilft
- Struktur (Structure): grafische Darstellung der Klassen des Entwurfsmusters, meist UML-basiert
- Beteiligte (Participants): Klassen und Objekte, die in die Anwendung des Musters involviert sind
- Zusammenspiel (Collaborations): Beschreibung der Zusammenarbeit zwischen den Beteiligten
- Konsequenzen (Consequences): Ergebnisse sowie Vor- und Nachteile der Anwendung des Musters
- Implementierung (Implementation): Beschreibung von Besonderheiten und möglichen Problemen bei einer Implementierung des Musters
- Codebeispiele (Sample Code): Das GoF-Buch verwendet C++ und/oder Smalltalk; in neueren Büchern und Websites wird meist Java oder C# benutzt. Prinzipiell kann jede objektorientierte Programmiersprache zum Einsatz kommen.
- Einsatzbeispiele (Known Uses): Beispiele für die Anwendung dieser Muster in realen Softwaresystemen
- Querverweise (Related Patterns): Zusammenarbeit dieses Entwurfsmusters mit anderen Mustern; gegebenenfalls Gemeinsamkeiten und Unterschiede
Der Originalkatalog aus dem Gang-of-Four-Buch
Das Buch »Design Patterns« enthält einen Katalog von 23 Mustern, die nach diesem Schema aufgelistet werden. Es handelt sich um Probleme, vor denen eines Tages jeder steht, der größere objektorientierte Programme schreiben möchte. Es hält Sie allerdings nichts davon ab, Ihre eigenen gelungenen Lösungsansätze ebenfalls nach diesem Schema zu katalogisieren. Wenn Sie einem Entwicklungsteam angehören, könnten Ihre Kollegen Ihnen eines Tages dafür dankbar sein.
In sehen Sie eine Kurzübersicht über die 23 GoF-Muster. Die Reihenfolge hält sich an diejenige im Buch: Die Muster werden innerhalb jeder der drei Purpose-Bereiche alphabetisch sortiert. Die Inhalte der Spalten »Name (Aliasse)« und »Einordnung« entsprechen der Beschreibung aus der zuvor dargestellten Aufzählung; die Spalte »Beschreibung« schildert kurz und knapp, was das jeweilige Muster leistet, enthält also die Informationen aus dem Katalogabschnitt »Absicht« und gegebenenfalls ein paar zusätzliche Hinweise. Im nachfolgenden Abschnitt finden Sie ein vollständig ausgeführtes Beispiel für ein GoF-Entwurfsmuster.
In den Pattern-Beschreibungen taucht des Öfteren der Begriff Client auf. Dabei handelt es sich in aller Regel nicht um einen Netzwerkclient, sondern um diejenige Klasse oder das Objekt, das sich der Dienstleistung des jeweiligen Patterns bedient.
Beispiel: Das Singleton-Pattern
Beim Singleton-Pattern handelt es sich um ein Muster zur Verwirklichung eine Klasse, von der nur genau eine einzige Instanz existieren darf.
- Name: Singleton
- Einordnung: Erzeugungsmuster, Objekt
- Absicht: Sicherstellen, dass eine Klasse nur genau eine einzige Instanz hat, und den globalen Zugriff auf diese ermöglichen
- Alias: keines (deutsche Bezeichnung: Einzelstück)
- Motivation: Bestimmte Objekte darf es selbst im größten System nur ein einziges Mal geben. Denken
Sie beispielsweise an eine zentrale Warteschlange für Datei-, Drucker- oder Netzwerkzugriffe
oder an eine globale Log-Datei für Ereignisse aus verschiedenen Programmbereichen.
Praktischerweise wird ein solches Element als Klasse erstellt, die nur beim ersten
Aufruf eine neue Instanz erzeugt und bei späteren Aufrufen immer wieder einen Verweis
auf diese Instanz zurückgibt. So brauchen Sie beim Aufruf nicht mehr zu überprüfen,
ob die Instanz bereits existiert.
Daneben kann der Singleton auch als bessere globale Variable dienen, weil die Instanz auf einfache Weise global verfügbar ist.
- Verwendungszweck: Benutzen Sie dieses Muster, wenn Sie eine erweiterbare Klasse brauchen, die ohne Modifikation des aufrufenden Codes nur genau eine Instanz besitzen darf.
- Struktur: Die Struktur der Klasse Singleton wird in Abbildung 11.9 dargestellt.
Abbildung 11.9 UML-Struktur der Klasse Singleton und ihrer einzigen Instanz
- Beteiligte: Das einzige Element ist die Klasse Singleton selbst, die auf Anforderung ihre einzige Instanz zurückgibt und gegebenenfalls neu erzeugt.
- Zusammenspiel: Andere Klassen rufen die Methode instance() auf, um die einzige Instanz der Klasse zu erhalten.
- Konsequenzen: Das Entwurfsmuster Singleton bietet eine Reihe von Vorteilen gegenüber anderen Lösungen.
Hier die wichtigsten Vorteile:
- Die Verwendung einer globalen Variablen wird vermieden; dies beseitigt eine potenzielle Fehlerquelle.
- Statt genau einer Instanz können Sie mithilfe dieses Musters auch eine beliebige andere (festgelegte) Anzahl oder auch Höchstzahl von Instanzen zulassen.
- Die Klasse bleibt erweiterbar – im Gegensatz zu anderen Lösungsansätzen für dieses Problem können problemlos abgeleitete Klassen gebildet werden.
- Implementierung: Die Instanz wird zum statischen Attribut der Klasse selbst und mit null (noch keine Instanz vorhanden) initialisiert. Der Konstruktor wird mit der Veröffentlichungsstufe private versehen, sodass er nicht von außen aufgerufen werden kann. Die öffentliche Methode instance(), die Clients stattdessen aufrufen können, überprüft zunächst, ob die Instanz bereits erzeugt wurde; falls nicht, ruft sie den Konstruktor auf. Anschließend wird in jedem Fall eine Referenz auf die Instanz zurückgegeben.
- Codebeispiele: Die Implementierung der Klasse Singleton ist nicht besonders umfangreich. Hier eine vollständige Java-Klasse, die dem Entwurfsmuster
genügt:
public class Singleton {
// Die Instanz - zunächst noch nicht vorhanden
private static Singleton singleton = null;
// der private Konstruktor
private Singleton() {
}
// die Client-Methode instance()
public static synchronized Singleton instance() {
// Instanz erzeugen, falls noch keine existiert
if (singleton == null) {
singleton = new Singleton();
}
// Instanz auf jeden Fall zurückgeben
return singleton;
}
}Der Modifikator synchronized bei der Methode instance() ist wichtig: Wenn mehrere Threads die Methode parallel aufrufen, könnten sonst versehentlich doch mehrere Instanzen erzeugt werden.
In Ruby lässt sich der Singleton beispielsweise wie folgt implementieren:
class Singleton
# Die Instanz als Klassenvariable,
# zunächst leer
@@instance = nil
# Konstruktor und Instanzmethode clone
# privat setzen
private_class_method :new
private :clone, :dup
# Die einzige Instanz zurückgeben
def Singleton.get_instance
if @@instance == nil
@@instance = new
end
@@instance
end
endDies ist eine recht »paranoide« Implementierung (und somit ein relativ narrensicherer Singleton). In Ruby existieren für alle Objekte die Methoden clone und dup, mit denen sich eine Kopie einer vorhandenen Instanz – und damit ebene eine weitere Instanz der zugrunde liegenden Klasse – erzeugen lässt. Deshalb wird hier nicht nur der Konstruktor, sondern es werden auch diese Methoden privat gesetzt.
Beachten Sie in diesem Zusammenhang, dass der eigentliche Konstruktor new und nicht etwa initialize heißt. Letzteres ist eine Methode, die bei der Objekterzeugung automatisch aufgerufen wird, um die Attribute zu initialisieren. Außerdem ist es wichtig, dass new eine Klassenmethode, clone dagegen eine Instanzmethode ist. Deshalb müssen unterschiedliche Schlüsselwörter verwendet werden, um sie zu privaten Methoden zu machen.
Ruby macht es Ihnen allerdings noch leichter: Die Standardbibliothek enthält eine Bibliotheksdatei namens singleton. Das darin enthaltene Modul Singleton brauchen Sie nur in Ihre eigene Klassendefinition zu inkludieren, und schon ist Ihre Klasse ein threadsicherer Singleton, bei dem clone und andere »gefährliche« Methoden geschützt wurden. Die automatisch bereitgestellte Methode zur Instanzerzeugung heißt instance. Hier ein Einsatzbeispiel:
# Bibliothek "singleton" importieren
require "singleton"
class MySingleton
# Modul Singleton inkludieren
include Singleton
end - Einsatzbeispiele: unzählige – alle künstlichen »Engpässe« wie beispielsweise Warteschlangen folgen diesem Schema.
- Querverweise: Entwurfsmuster wie Abstract Factory, Builder und Prototype lassen sich mithilfe des Singleton-Patterns implementieren.
11.2.3 Unit-Tests
Wenn die Zeit in Softwareentwicklungsprozessen eng wird, verzichten die Entwickler am ehesten auf ausgiebige Tests. Das ist fatal für die Qualität der veröffentlichten Anwendungen – der Extremfall ist sogenannte Banana Ware, die »grün« ausgeliefert wird und erst »beim Kunden reift«. Teure kommerzielle Software ist sogar häufiger von diesem Problem betroffen als Open-Source-Projekte, weil die Marketing- und Vertriebsabteilungen oftmals massiven Druck auf die Entwicklungsteams ausüben, um angekündigte Veröffentlichungstermine einzuhalten.
Natürlich haben Entwickler sich seit Jahrzehnten Gedanken darüber gemacht, wie sich die unbefriedigende Situation im Bereich der Softwaretests verbessern ließe. Eine wichtige Erkenntnis ist, dass Programmcode sich am exaktesten durch weiteren Programmcode überprüfen lässt. Anstatt sich also den Kopf darüber zu zerbrechen, welche Fehler auftreten könnten, und mühsam von Hand entsprechende Zustände herbeizuführen, sollten Sie einen automatisierten Test schreiben und mit verschiedenen Werten durchlaufen lassen.
Die neueste Lösung zur Testautomatisierung sind die sogenannten Unit-Tests oder auch Klassentests. Für beinahe jede wichtige Programmiersprache steht inzwischen ein xUnit-Framework zur Verfügung, das die Durchführung von Tests vereinfacht und beschleunigt und diese so zu einem integralen Bestandteil der Programmierarbeit macht. Der Klassiker ist das hier vorgestellte JUnit-Framework für Java, das von Erich Gamma und Kent Beck geschrieben wurde.
Der erste Schritt besteht darin, JUnit herunterzuladen und in Betrieb zu nehmen. Besuchen Sie die Projekt-Website http://www.junit.org, und laden Sie das aktuelle Paket herunter (zurzeit junit4.9.zip). Entpacken Sie das Archiv in ein beliebiges Verzeichnis, und erweitern Sie Ihren CLASSPATH so, dass er junit.jar aus diesem Verzeichnis enthält. Nun können Sie das Framework direkt einsetzen.
Ein einfaches Testbeispiel
Betrachten Sie als Beispiel die folgende Klasse Artikel. Sie enthält drei Methoden, die den Bruttopreis, den Mehrwertsteuerbetrag und den Nettopreis zurückgeben sollen:
public class Artikel {
private double preis;
private int mwst;
public Artikel () {
this.preis = 0;
this.mwst = 19;
}
public Artikel (double p, int m) {
this.preis = p;
this.mwst = m;
}
public double getBrutto () {
return this.preis;
}
public double getMwst () {
return this.preis / (100 + this.mwst) * this.mwst;
}
public double getNetto () {
return this.preis - this.getMwst();
}
}
Angenommen, Sie möchten die ordnungsgemäße Funktion der Methode getNetto() testen. Dazu können Sie mithilfe von JUnit folgenden Unit-Test schreiben:
import junit.framework.*;
public class ArtikelTest extends TestCase {
public ArtikelTest (String name) {
super (name);
}
public void testNetto() {
Artikel a1 = new Artikel (119, 19);
assertTrue (a1.getNetto() == 100);
}
public static void main(String[] args) {
junit.swingui.TestRunner.run (ArtikelTest.class);
}
}
Das Package junit.framework enthält die JUnit-Testklassen, die hier verwendet werden. Jeder Test sollte die Klasse TestCase erweitern. Wichtig ist hier ein Konstruktor, der einen String als Parameter erwartet. Sie können ihn, wie im Beispiel, einfach an den entsprechenden Konstruktor der übergeordneten Klasse weiterreichen.
main() ruft die Methode run() von junit.swingui.TestRunner auf, sodass beim Ausführen der Klasse automatisch die GUI-Variante von JUnit ausgeführt wird. run() sorgt automatisch für die Ausführung sämtlicher Methoden, die mit test*() beginnen. Dabei gibt es ein sehr deutliches Zeichen für Erfolg oder Misserfolg:
- Ein grüner Balken zeigt, dass der Test bestanden wurde.
- Ein roter Balken bedeutet Misserfolg; im unteren Fensterabschnitt werden die entsprechenden Fehlermeldungen angezeigt.
Der Test selbst funktioniert folgendermaßen: Die Annahme ist, dass ein Bruttopreis von 119 € beim üblichen Mehrwertsteuersatz von 19 % zu einem Nettopreis von 100 € führt. Also wird die Testmethode assertTrue() mit dem Vergleich zwischen dem Ergebnis von getNetto() und dem Wert 100 aufgerufen. Falls die Vermutung richtig sein sollte, führt sie zu einem grünen Balken (in Abbildung 11.10 wird dieses erfreuliche Ergebnis gezeigt). Das Framework definiert übrigens noch weitere assert*()-Methoden, weil Erfolg nicht in jedem Fall durch ein »richtiges« (oder besser »wahres«) Ergebnis angezeigt wird.
Abbildung 11.10 Erfolgreicher Test der Klasse »Artikel« in JUnit
Das Test-first-Verfahren
Damit der Test auf keinen Fall »vergessen« werden kann, empfiehlt es sich, ihn nicht etwa nach der Implementierung eines Features, sondern vorher zu schreiben. Das hat natürlich zur Folge, dass er zunächst nicht bestanden wird. Daraus ergibt sich die Arbeitsweise von Test-driven Development (TDD), auf Deutsch: testgetriebene Entwicklung:
- Red – einen Test schreiben, der zunächst fehlschlägt (roter Balken)
- Green – Code schreiben, der den Test mit den einfachsten möglichen Mitteln besteht
- Refactor – den neuen Code durch Refactoring vernünftig integrieren, zum Beispiel unnötige Doppelanweisungsfolgen vermeiden
Statt durch viel Theorie lässt sich der Test-first-Ansatz am einfachsten an einem Beispiel zeigen: Die Klasse Artikel aus dem vorigen Abschnitt soll um eine Methode namens getDMBrutto() erweitert werden, die den Bruttopreis in DM ausgibt (praktisch für die in den vergangenen Jahren, besonders in der Vorweihnachtszeit, in manchen Geschäften veranstalteten »Zahl mit DM«-Aktionen).
Zunächst wird also ein einfacher Test geschrieben:
import junit.framework.*;
public class DMTest extends TestCase {
public DMTest (String name) {
super (name);
}
public void testDMBrutto() {
Artikel a = new Artikel (100, 19);
assertTrue (a.getDMBrutto() == 195.583);
}
public static void main(String[] args) {
junit.swingui.TestRunner.run (DMTest.class);
}
}
Da der Umrechnungsfaktor bekannt ist, lässt es sich leicht vorhersagen, welcher Wert für 100 € herauskommen muss. Diese Vermutung wird als Testfall formuliert.
Damit der Test sich überhaupt kompilieren lässt, wird zumindest ein »Dummy« der Artikel-Methode getDMBrutto() benötigt. Dieser könnte beispielsweise so aussehen:
public double getDMBrutto () {
return 0;
}
In einem so offensichtlichen Fall bräuchten Sie den Test noch nicht einmal auszuführen, um zu wissen, dass er scheitern wird. Tun Sie es trotzdem – nur so gewöhnen Sie sich an den Test-first-Ablauf.
Der nächste Schritt besteht darin, sicherzustellen, dass der Test bestanden wird. Der erste Ansatz darf ruhig eine »Brute Force«-Methode (rohe Gewalt) sein. Wenn beispielsweise 195.583 verlangt wird, kann dieser Wert einfach zurückgegeben werden:
public double getDMBrutto () {
return 195.583;
}
Nun erscheint der erwartete grüne Balken. Jetzt braucht die Methode nur noch verallgemeinert zu werden, damit sie für beliebige Werte das richtige Ergebnis liefert; das ist die für diesen Fall geeignete Form des Refactorings. So ergibt sich folgende Endfassung:
public double getDMBrutto () {
return this.preis * 1.95583;
}
Nach dem Refactoring sollten Sie den Test natürlich noch einmal durchführen, um sicherzustellen, dass Sie sich nicht vertan haben.
Auf diese Weise können Sie ein Projekt Test für Test aufbauen. In seinem Buch »Test-driven Development by Example« vergleicht Kent Beck diese Arbeitsweise mit dem Heraufziehen eines Eimers aus einem Brunnen, bei dem die Kurbelwelle mit Zähnen ausgestattet ist, die beim Loslassen einrasten. Genau dies verspricht das Test-driven Development: zu jeder Zeit »clean code that works«, also jederzeit ein so gut wie releasefähiges Projekt.
Einen guten Einstieg in die Arbeit mit JUnit bietet der lesenswerte Aufsatz »Test-Infected: Programmers Love Writing Tests« (http://junit.sourceforge.net/doc/testinfected/testing.htm), der übrigens auch mit der Offlinedokumentation von JUnit mitgeliefert wird.
Weitere Beispiele zur testgetriebenen Entwicklung finden Sie übrigens in Kapitel 18, »Webserveranwendungen«; dort kommt das PHP-Test-Framework PHPUnit zum Einsatz.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.