Programmiertechnik/Entwicklung wiederverwendbarer Software

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der vorliegende Artikel soll dem Leser ein Gefühl für die Entwicklung wiederverwendbarer Software geben. Als "wiederverwendbar" werden im allgemeinen Softwarebestandteile bezeichnet, die in mehreren Programmierprojekten immer wieder eingesetzt werden können. In vielen Sprachen nennt man das "Bibliotheken", "Klassen" oder auch "Module". Dieser Artikel richtet sich hauptsächlich an Menschen, die bereits ein wenig Erfahrung mit dem Programmieren gesammelt haben, aber ihre Kenntnisse erweitern und ihre Techniken verfeinern möchten.

Der Artikel ist der Versuch, dem geneigten Leser Konzepte zur modularen Softwareentwicklung näher zu bringen, ohne zu sehr in blanke Theorie zu verfallen.

Einführung

Ein weit verbreitetes Vorurteil ist die Auffassung, dass Programmierer faul sind. Dies wird sogar oft bestätigt. Und zwar vor allem durch die erfahreneren unter den Softwareentwicklern. Sie besitzen nämlich die Fähigkeit, jede Codezeile mit dem Bewusstsein zu schreiben, dass sie woanders auch mal nützlich sein könnte. Sie können wiederverwendbare Software produzieren. Sie sind zu faul, ein und die selbe Funktion zweimal zu schreiben. Ein einfaches Beispiel: mit Sprachen wie Perl, PHP oder auch JavaScript soll hauptsächlich HTML ausgegeben werden. Diese Aufgabe wird letztendlich durch Befehle wie print, echo und document.write wahrgenommen. In der Praxis treten aber immer wieder Fälle auf, in denen ein Formularfeld, eine Tabelle, ein bestimmtes Konstrukt oder spezielle Meta-Angaben ausgegeben werden müssen. Was liegt näher, als die Funktionalität der "Ausgabe von HTML" in einer Gruppe von Funktionen so zusammen zu fassen, dass man sie in weitere Projekte ganz einfach "mitnehmen" kann? Und unter "mitnehmen" wird hier nicht einfach cut & paste verstanden, sondern die direkte Einbindung der Funktionen als Includedateien in andere Projekte.

Erste Überlegungen

Wie kommt man dazu, wirklich wiederverwendbaren Code zu produzieren? Zunächst einmal sei gesagt, dass es nicht damit getan ist, ein paar Programmzeilen in eine Funktion und ein paar solcher Funktionen in eine separate Datei zu packen. Schließlich geht es um Funktionen, die möglichst nicht bei jedem Projekt komplett umgeschrieben werden sollen.

Bei einer konkreten Programmieraufgabe gibt es immer so etwas wie einen Plan, ein Konzept oder zumindest eine Anforderung, was das Programm leisten soll. Gerade beim Skripting für Webanwendungen besteht häufig die Möglichkeit, ein Programm einfach Zeile für Zeile aneinander zu reihen, oft sogar, ohne auch nur eine einzige Subroutine zu verwenden. Und sehr oft ist dies auch die schlechteste Möglichkeit, sich einer Programmieraufgabe zu stellen. Eine sinnvolle Vorüberlegung wäre das Herausfiltern einzelner Funktionsgruppen.

Nehmen wir als einfaches Beispiel ein simples Gästebuchskript. Ein solches Programm könnte man ohne Probleme ohne auch nur eine einzige Subroutine schreiben. Die Arbeit würde sich allerdings nur auf dieses eine Programm fixieren. Der geschriebene Code könnte nicht (oder nur sehr ineffizient) weiterverwendet werden. Deshalb sollte man sich über eine Programmieraufgabe ein wenig mehr Gedanken machen. Bei näherer Betrachtung fällt auf, dass ein Gästebuchskript mindestens folgende Aufgaben zu bewältigen hat:

  • Ausgabe der Benutzerschnittstelle (HTML-Anzeige des Eingabeformulars und des Gästebuchinhalts, evtl. eine Verwaltungsoberfläche)
  • Prüfung der Eingaben von Benutzern
  • Datenverarbeitung
  • Speicherung und Verwaltung von Daten

Diese Liste macht schon eines sehr deutlich: solche oder so ähnliche Aufgaben könnten auf viele andere Programme auch zu kommen, beispielsweise ein Forumsskript oder ein Programm für einen Newsletter. Allen sind diese grundlegenden Funktionsgruppen gemeinsam.

Abgrenzung logischer Einheiten

Als guter Anfang hat sich die Entwicklung von logisch abgegrenzten Funktionen erwiesen. Diese Funktionen kann man später in Gruppen einteilen, die der obigen Aufgabenliste entsprechen. Bei der Entwicklung von Funktionen bzw. Subroutinen sollte man aber einiges beachten, will man sie später in einer Funktionsbibliothek einsetzen. Wie der Name Subroutine schon vermuten lässt, sollte es sich dabei um ein abgegrenztes Stück Programmcode handeln, der eine ganz bestimmte Funktion erfüllt.

Diese Abgrenzung erzielt man, indem man einer Subroutine Werte übergibt und wieder etwas von ihr zurück "verlangt". Eine Subroutine sollte niemals auf globale Variable zugreifen, sondern die Arbeitswerte immer über die Argumentenliste erwarten. Genauso sollte sie niemals in globale Variable schreiben, sondern das Ergebnis zurückgeben. Für die Rückgabe wird in vielen Sprachen das Schlüsselwort return eingesetzt. Subroutinen, die ihre Arbeitswerte aus dem globalen Namensraum des Hauptprogramms beziehen, oder Ergebnisse in diesen zurückschreiben, erweisen sich als unhandlich.

Subroutinen sind außerdem unhandlich, wenn sie mit Ihrem Ergebnis eine ganz bestimmte Aktion ausführen, beispielsweise es sofort ausgeben. Wenn etwas anderes als die Ausgabe gewünscht wird, ist eine solche Subroutine bereits unbrauchbar. Die nachfolgenden Beispiele sollen den Unterschied deutlich machen.

Hinweis: die folgenden Funktionen in Wirklichkeit zu implementieren, ist natürlich überflüssig, da die meisten Programmiersprachen für diese Aufgaben Operatoren anbieten. Die Beispiele wurden so einfach gewählt, um den Unterschied zwischen handlichen und unhandlichen Subroutinen deutlich zu machen. Beispiel einer unhandlichen Subroutine:

Beispiel
//globale Variable
$a = 5;
$b = 3;
$ergebnis = 0;

//Aufruf der Funktion
summe();

//Definition einer "unhandlichen" Funktion
function summe() {
	global $a, $b;
	$ergebnis = $a + $b;
	
	//Ausgabe des Ergebnisses
	echo $ergebnis;
}

Die unhandliche Funktion summe() ist nur bedingt wiederverwendbar. Das liegt an dem direkten Zugriff auf den globalen Namensraum des Hauptprogramms. Jedes Programm, das die Funktion verwenden will, muss die Variablen $a, und $b freihalten. Zusätzlich kann die Funktion nichts weiter, als das Ergebnis zu berechnen und anschließend sofort auszugeben. Stellen Sie sich das Chaos vor, wenn alle vier Grundrechenarten auf diese Weise implementiert würden!


Beispiel einer wiederverwendbaren Subroutine:
//globale Variable
$meine_summe = 0;
$wert_a = 5;
$wert_b = 3;

//Aufruf der Funktion
$meine_summe = summe($wert_a, $wert_b);

//Ausgabe des Ergebnisses
echo $meine_summe;

//Definition einer wiederverwendbaren Funktion
function summe($a, $b) {
	//Berechnung der Summe der beiden Argumente
	$ergebnis = $a + $b;
	
	//Rückgabe des Berechnungsergebnisses
	return $ergebnis;
}

Diese Subroutine ist wiederverwendbar. Der Programmierer des Hauptprogramms hat die freie Wahl bei Variablennamen und ihm bleibt es überlassen, wie er das Ergebnis weiterverwendet. Es könnte ja auch sein, dass nur das Berechnungsergebnis benötigt wird, aber keine Ausgabe.

Beachten Sie: Trotz der genauen Einhaltung dieser Grundregeln kann es manchmal vorkommen, dennoch auf globale Variable zugreifen zu müssen. In PHP könnte dies z.B. das globale $_POST-Array sein, in Perl beispielsweise die Standardvariable $_. Wenn es sich um Variablen handelt, welche die Programmiersprache selbst im globalen Namensraum bereit stellt, so kann man schon Ausnahmen machen. Allerdings ist es wichtig, zu erkennen, ob dies wirklich notwendig ist. Denn alles was z.B. in $_POST steht, kann man einer Funktion auch als Argument übergeben!

Auslagerung in Bibliotheksdateien

Hat man den Sinn von Subroutinen einmal verstanden, so entstehen diese ganz von selbst in den meisten Programmen. In unserem Gästebuchskript entstehen so vielleicht Funktionen zum Codieren von HTML-Sonderzeichen, zur Ausgabe von Formularelementen wie Optionsfeldern und Textareas, zur Speicherung eines Datensatzes auf der Festplatte und viele mehr. Hat die Anzahl der vorkommenden Subroutinen eine gewisse Grenze überschritten, so ist es angebracht, sie logisch zu gruppieren und in gesonderte Dateien auszulagern. In unserem Fall könnte auf diese Weise eine Datei für alle HTML-Ausgabefunktionen und eine für alle datenorientierten Funktionen entstehen.

Die meisten Programmiersprachen bieten die Möglichkeit an, solche ausgelagerten Dateien einzubinden. In Perl geschieht dies z. B. mit der use-Anweisung, in PHP mit include oder require. Bei JavaScript hingegen muss nur die entsprechende .js-Datei auf der Webseite eingebunden sein.

Sind die Funktionen erst mal in gesonderte Dateien ausgelagert worden, so beginnt der schwierige Teil in der Entwicklung wiederverwendbarer Software. Die einzelnen Subroutinen müssen ein Mindestmaß an Abstraktion aufweisen, um sinnvoll weiterverwendet werden zu können. Beispielsweise ist eine Funktion zur Speicherung eines Datensatzes schon recht schön. Aber wenn man damit nur Name, E-Mail-Adresse und einen Text speichern kann, ist das Einsatzgebiet auch schon wieder relativ beschränkt. Eine Funktion für die Ausgabe eines Texteingabefelds mag praktisch erscheinen. Wenn man aber nur den Feldnamen bestimmen kann und eine feste Breite vorgegeben ist, so werden nur wenige Projekte von dieser Funktion profitieren.

Der Vorgang des Abstrahierens ist eine ziemliche Gratwanderung. Eine einzelne Subroutine kann immer nur ein gewisses Spektrum an Anwendungsgebieten abdecken. Je breiter dieses Spektrum bei einem einigermaßen kompakten Code ist, umso besser. Aber irgendwo stößt jede Subroutine an ihre Grenzen. In jedem Fall aber sollte beim Design einer Subroutine daran gedacht werden, dass diese weiterentwickelt werden kann. Im Idealfall lässt sich eine Funktion so weiterentwickeln, dass man sie sogar noch in älteren Programmen einsetzen kann, ohne den Code des Hauptprogramms verändern zu müssen.

Die Subroutinen in unseren Bibliotheksdateien sollten also ein Mindestmaß an Flexibilität und Erweiterbarkeit aufweisen. Deshalb kann es manchmal erforderlich sein, bereits beim Projekt A Subroutinen zu schreiben, die mehr können, als eigentlich gefordert wird. Diese Zeit ist meist gut investiert, da sie bei Projekt B oder C schon wieder eingespart werden kann. Im Laufe der Zeit werden sich die Funktionen der Bibliothek weiterentwickeln und verfeinern. Optimal ist, wenn eine Subroutine beim Projekt D bereits viele Male überarbeitet und verbessert wurde, aber trotzdem noch in Projekt A ohne Quellcodeveränderung einsetzbar ist. Dieses Verhalten nennt sich Abwärtskompatibilität.

Dokumentation und Versionierung

Nachdem wir nun bei einem Projekt mehrere Bibliotheksdateien erstellt haben, stellt sich die Frage, wie man damit weiter umgeht. Als erstes sollte man an die Versionierung der Bibliotheken denken. Eine Versionsnummer, die bei allen Änderungen am Code erhöht wird, gibt deutlich Auskunft über den Bearbeitungsstand. Dazu sollten in einer gesonderten Datei zu jedem Versionssprung die Änderungen dokumentiert werden. Dieser Aufwand erscheint anfangs beträchtlich. Ist eine Bibliothek aber erst einmal ein paar Monate oder gar Jahre im Einsatz, so zeigt sich schnell, dass er sich lohnt. Denn nichts ist schlimmer, als einige Kopien der Bibliotheksdateien zu haben, ohne zu wissen, welche nun die aktuelle ist.

Ein weiterer wichtiger Aspekt bei der Entwicklung wiederverwendbarer Software ist die Dokumentation. Einige Entwickler haben ja regelrechte Phobien dagegen entwickelt. Aber eine gute Dokumentation der Bibliotheksfunktionen ist nicht nur für andere Entwickler von Nutzen. Wenn Sie eine eigene Bibliothek einige Monate nicht mehr genutzt haben, werden Sie froh sein über jedes Stückchen Text, das Ihnen Ihr eigenes Werk wieder in die Erinnerung ruft. Zu einer guten Bibliotheksdokumentation gehört eine Beschreibung aller Funktionen und ihres Zusammenspiels. Zu jeder Funktion sollte dokumentiert sein, welche Argumente sie entgegen nimmt und was die Funktion zurückgibt. In bekannten Dokumentationen, wie z. B. der PHP-Dokumentation ist dies recht gut gelöst:

Beispiel einer Funktionsbeschreibung:
string str_replace ( string needle, string str, string haystack)
Diese Funktion ersetzt alle Vorkommen von needle innerhalb der Zeichenkette haystack durch den String str.

Das Beispiel wurde der aktuellen PHP-Dokumentation entnommen. Vor dem Funktionsnamen wird der Datentyp des Rückgabewerts notiert. Danach folgen in Klammern die benötigten Argumente (zuerst die Datentypen, dann die Namen). Optionale Argumente werden in eckige Klammern gefasst. Die Argumentennamen dienen dazu, sie im Text näher beschreiben zu können. Häufig wird im Text auch beschrieben, wie die Funktion sich im Fehlerfall verhält.

Das API (Application Programming Interface)

Jede Programmierbibliothek hat eine Schnittstelle für den Entwickler des Hauptprogramms. Das so genannte API ist das Bindeglied zwischen dem Entwickler des Hauptprogramms, welches auf der Bibliothek aufsetzt, und der Bibliothek selbst. Diese Schnittstelle definiert sich durch die Art und Weise, wie die Bibliotheksfunktionen Argumente entgegen nehmen, auf Fehler und Ausnahmen reagieren und auf welche Weise sie die Ergebnisse zurückgeben.

Will eine Bibliothek wirklich wiederverwendbar und abwärtskompatibel sein, sollte das API gut durchdacht sein. Es sollte so flexibel angelegt sein, dass spätere Erweiterungen implementiert werden können, ohne das API selbst zu verändern. Außerdem sollte das API konsequent durchgezogen werden. Beispielsweise sollte die Reihenfolge ggf. erwarteter Funktionsargumente gut durchdacht sein. Wenn eine Funktion in eine Datei schreibt und eine andere aus einer Datei liest, dann sollte der Dateiname und das, was geschrieben oder gelesen werden soll, bei beiden Funktionen an der selben Stelle erwartet werden. Oder wenn Sie in der objektorientierten Programmierung eine Klasse für HTML-Erzeugung und eine Klasse für XML-Erzeugung haben, sollten Methoden, die das gleiche bewirken auch den gleichen Namen haben. Also nicht in der einen Klasse output() schreiben und in der nächsten Ausgabe().

Beispiel eines inkonsequenten API:
function writeTo($text, $FileName) {
	...
	return $line_number;
}

function ReadFROM($filename, $LINE_NUM) {
	...
	return $text;
}
Diese Funktion ersetzt alle Vorkommen von needle innerhalb der Zeichenkette haystack durch den String str.

Hier wurden gleich mehrere Fehler eingebaut. Zunächst einmal ist die Schreibweise der Funktions- und Variablennamen inkonsequent. Das ist zwar ein vergleichsweise harmloser Fehler, zeugt aber von schlechtem Programmierstil. Der Code wird schwerer zu lesen und die Handhabung der Funktionen ist nicht intuitiv. Der Benutzer der Bibliothek muss ständig nach der richtigen Schreibweise grübeln.

Die Reihenfolge der sinnvoll zusammengehörigen Argumente wurde ebenfalls nicht eingehalten. Während man bei der Schreibfunktion zunächst den Text und dann Dateinamen übergeben muss, ist die bei der Lesefunktion unsinnigerweise genau umgekehrt.


Beispiel eines konsequenten API:
function write_to($filename, $text) {
	...
	return $line_number;
}

function read_from($filename, $line_number) {
	...
	return $text;
}
Die beiden Funktionen aus diesem Beispiel sind im Sinne eines konsequenten API sinnvoll angelegt. Sie sind übrigens komplementär; das, was eine Funktion zurückgibt, kann der anderen wieder als Argument übergeben werden. Insbesondere bei solchen Funktionen ist auf Einheitlichkeit zu achten.

Eigenschaften bei objektorientierter Entwicklung

Ein weiterer, wichtiger Aspekt insbesondere bei der objektorientierten Entwicklung ist der Zugriff auf Objekteigenschaften (die Variable eines Objekts). Das API einer Klasse sollte vom Programmierer niemals erwarten, direkt lesend oder schreibend auf Eigenschaften des Objekts zuzugreifen. Für diese Funktionalität sollten Sie immer geeignete Methoden (z. B. get_irgendwas() und set_irgendwas()) bereit stellen. Auch wenn es vielleicht als sinnlose Fleißaufgabe erscheint, so schafft dieses Vorgehen eine viel größere Flexibilität bei der Pflege des APIs. Denn Sie als Bibliotheksentwickler können in späteren Versionen Eigenschaftsnamen nach Ihrem Belieben verändern, ohne die Abwärtskompatibilität einzubüßen. Weiterhin haben sogar noch die Kontrolle über Lese- und Schreibzugriffe auf Eigenschaften (die get_*- und set_*-Methoden können ja mehr Code enthalten, als nur den zum Zurückgeben und Schreiben einer Eigenschaft.

Bedeutung des API

Je weiter sich eine Bibliothek fort entwickelt, desto gewichtiger wird die Bedeutung des API. Werden bei einem Release grundlegende Änderungen am API vorgenommen, so können ältere Programme nicht mehr von der neuen Version (die u.U. fehlerbereinigt ist) profitieren. Zumindest nicht, ohne umgeschrieben zu werden.

Ein gutes API ist vielleicht der wichtigste Bestandteil wiederverwendbarer Software. Auf folgende Punkte ist insbesondere beim Design und der Implementation des APIs zu beachten:

  • intuitive Bedienung (wird erreicht durch die Einhaltung der Designrichtlinien)
  • einheitliche Designrichtlinien (Namensgebung für Subroutinen und Variable, Reihenfolge von Argumenten, Art und Weise der Fehlerbehandlung und der Rückgabe von Ergebnissen)
  • ausführliche und vollständige Dokumentation (detaillierte Funktionsbeschreibungen, Anwendungsbeispiele, Referenzteil)
  • neue Funktionsargumente müssen optional sein (so wird Abwärtskompatibilität überhaupt erst möglich)
  • keine überfrachteten Subroutinen (Ein Subroutine soll ihre Aufgabe beherrschen und sich darauf konzentrieren; für neue Funktionen sollten neue Subroutinen eingeführt werden)

Das Namensraumproblem

Mit ein wenig Übung kann man unter Berücksichtigung all der hier genannten Aspekte schon relativ gut wiederverwendbare Software schreiben. Eine der übriggebliebenen Schwierigkeiten ist die Vergabe von Namen für die Subroutinen. Jede definierte Funktion innerhalb eines Programms muss ja einen eindeutigen Namen besitzen. Wird in zwei Bibliotheken der gleiche Funktionsname vergeben (z. B. init()), dann führt das bei gleichzeitiger Verwendung der beiden Bibliotheken zu einem Fehler.

An dieser Stelle sind die Fähigkeiten der jeweilig eingesetzten Programmiersprache gefragt. Es gibt Unterschiede zwischen den einzelnen Sprachen in Bezug auf die Unterstützung von Bibliotheken. In Perl beispielsweise können Sie für Ihre Bibliotheken eigene Module entwickeln, die durch sogenannte Pakete vollständig getrennte Namensräume besitzen. Die Variable $a im Paket main ist also völlig unterschiedlich zur Variable $a im Paket foo. In PHP gibt es analog dazu das Konzept der Namespaces.

Es kommt also darauf an, die Möglichkeiten einer Sprache in Bezug auf modulare Entwicklung zu kennen und auszunutzen. In den meisten Fällen genügt es jedenfalls nicht, einfach nur Dateien mit "Funktionssammlungen" zu erstellen. Bietet die Programmiersprache keine besonderen Möglichkeiten zur Definition getrennter Namensräume (z. B. bei strukturiertem PHP), so kann man sich auch geeignete und möglichst originelle Prefixe für Funktionsnamen überlegen. Beispielsweise könnte eine Funktion zur Ausgabe eines HTML-Optionsfeldes HTML_option() genannt werden. Es handelt sich also um eine Funktion aus der Gruppe HTML und der eigentliche Funktionsname lautet option(). Dies ist zwar nicht besonders elegant, aber immer noch besser, als Kollisionen mit anderen Funktionsnamen.

Essenz

Wiederverwendbare Software zu schreiben hat viele Vorteile. Gute Bibliotheken können nicht nur dem Entwickler selbst eine Arbeitserleichterung sein, sondern auch anderen Programmierern auf der ganzen Welt. Das Beispiel der Open Source Gemeinde macht dies sehr deutlich. Viele der Programme aus der Open Source Welt bauen auf Bibliotheken Dritter auf. Dieses Vorgehen macht es überhaupt erst möglich, ganze Betriebssysteme mit einem breiten Spektrum von Anwendungsprogrammen frei verfügbar zu machen. Wenn nämlich jeder Programmierer alleine im stillen Kämmerlein jede Funktion selbst schreiben müsste, so würde ein Großteil der verfügbaren Energie dafür verbraucht werden, Dinge zu tun, die andere schon tausendmal gemacht haben.

Viele frei verfügbare Bibliotheken sind zum Beispiel aus kommerziellen Projekten entstanden. Durch die Abgrenzung von Funktionseinheiten in separate Bereiche können einzelne Teile eines großen Projekts sehr leicht ausgegliedert werden. Dieses Vorgehen stellt eine win-win-Situation dar, weil Entwickler auf der ganzen Welt von solchen Bibliotheken profitieren und gleichzeitig über selbst geschriebene Erweiterungen oder Fehlerbereinigungen dem Autor wieder etwas zurückgeben (nämlich besseren Code).