Ich hatte ja schon angekündigt, dass ich mein Versprechen auf den letzten Delphi-Tagen in Raten erfüllen werde. Immer mal wieder landet etwas zu den Softwareentwicklungs-„Daumenregeln“ oder -Prinzipien hier im Blog. Vielleicht werde ich die Artikelserie irgendwann in ein Tutorial packen oder auf andere Weise verarbeiten. Mal sehen.
Zuerst aber ist mal ein grober Überblick notwendig und diesen möchte ich hier geben.
Wie schon mehrfach erwähnt, sehe ich Softwareentwicklung als das ständige Ausbalancieren von „Daumenregeln“ oder „Prinzipien“. Solche Daumenregeln gibt es eine ganze Menge. Ich hab bisher schon so an die hundert dieser Regeln zusammengetragen und werde die hoffentlich auch bald mal sortieren und hier posten. Diese Daumenregeln können sehr unterschiedlich sein. Manche sind sehr allgemein, andere hingegen auf ganz bestimmte Probleme spezialisiert. Viele dieser Daumenregeln, sind einfach nur Spezialisierungen oder Abwandlungen anderer, aber es ist auch nicht selten, dass sich einzelne Regeln widersprechen. Es ist dann jeweils ein geeigneter Mittelweg, ein Kompromiss zu suchen. Man kann diese Regeln auch als Kräfte betrachten, die in unterschiedliche Richtungen ziehen. So gesehen ist das Ziel ein Kräftegleichgewicht.
Das was ich hier schreibe, ist keine für alle Ewigkeiten gültige Wahrheit, an der nicht gerüttelt werden darf. Im Übrigen bin ich der Meinung, dass es so etwas gar nicht gibt. Aus nahe liegenden Gründen werde ich hier also meine persönliche Sichtweise auf die Softwareentwicklung darstellen. Man darf hier gerne anderer Meinung sein. Vieles ist Ansichtssache und auch wenn manche der Prinzipien (nicht aber alle) weithin anerkannte Lehrmeinung sind, sollte man trotzdem überlegen, ob man diese nachvollziehen kann. Im Übrigen wird man diese hier vorgestellten Daumenregeln nur dann wirklich anwenden können, wenn man die für zumindest einigermaßen sinnvoll erachtet.
Ich beziehe mich hier hauptsächlich auf objektorientierte Softwareentwicklung, prinzipiell gilt das meiste aber auch für andere Programmierparadigmen, bzw. sind leicht auf solche übertragbar. Jetzt will ich erstmal einen groben Überblick über die meiner Meinung nach wichtigsten Prinzipien geben. Genaueres folgt dann irgendwann mal in einzelnen Artikel zu den jeweiligen Prinzipien.
Die drei obersten Daumenregeln
„Die obersten drei Regeln der Softwareentwicklung“, wie ich sie hier mal nennen will, beschreiben, wie man mit den ganzen hier vorgestellten Regeln umgehen soll. So gesehen sind sie quasi Meta-Daumenregeln. Prinzipiell warnen sie davor, die Regeln zu dogmatisch zu sehen:
Wenn du meinst, eine Regel brechen zu müssen, dann tu es. Wundere dich aber nicht über die Konsequenzen.
Allein schon, weil sich manche Regeln gegenseitig widersprechen, kommt man nicht umhin, die ein oder andere Regel mal zu brechen. Das ist vollkommen normal und nicht zu ändern. Es gibt eben nicht die non-plus-ultra-Lösung die in jeglicher Hinsicht toll ist und keinerlei Nachteile hat. Alles hat seine Vor- und Nachteile.
Auf der anderen Seite gibt es die Regeln ja nicht ohne Grund. Wenn man also eine Regel bricht, dann hat das normalerweise Konsequenzen — eben die angesprochenen Nachteile. Man sollte diese also immer bedenken bzw. das Brechen der Regel als Erinnerung an diese Nachteile verstehen. Selbst, wenn man eine Regel nie beachtet und demnach immer bricht, so hat sie immer noch den Zweck auf die Nachteile hinzuweisen.
Wenn eine Regel besagt, dass etwas niemals getan werden darf, so bedeutet das „niemals“ niemals wirklich „nie“. Ebenso bedeutet „immer“ nie wirklich „immer“ und in jeder Situation. Es ist immer eine Abwägungssache.
Viele er Regeln hören sich sehr fordernd an. „Tue niemals XY“. Man sollte dabei eigentlich jeweils im Kopf ergänzen „außer du weißt, was du da tust.“ Wie schon gesagt: Regeln kann man brechen (Regel 1). Und das wird man auch des öfteren tun müssen.
Die Antwort auf alle Fragen heißt 42 „it depends“ bzw. „kommt drauf an“.
Es gibt kaum etwas das immer gilt, immer gut ist oder immer schlecht ist. Genauso wenig gibt es einen generell besten Ansatz. Das, was im einen Kontext gut in richtig sein kann, ist in einem anderen vollkommen falsch, blödsinnig und kontraproduktiv. Das, was im einen Fall noch in Ordnung ist, kann in nem anderen ne Katastrophe bewirken. Insbesondere kann man kleine und große Projekte in etwa genauso gut vergleichen wie das Bauen eines Vogelhäuschens mit dem eines Wolkenkratzers.
Interessant bei den obersten drei Daumenregeln ist noch, dass sie sich auf sich selbst anwenden lassen. Auch diese Regeln kann man brechen. Auch hier bedeutet „nie“ nicht „nie“. Und auch hier gilt immer „it depends“.
Ein paar Grundregeln
Allgemeine Prinzipien
Um ein komplexes Problem zu lösen, teile es in leicht lösbare Teilprobleme auf.
Grundlegendes Prinzip, dass quasi immer und überall eingesetzt wird. Viel zu sagen ist hier nicht. Die Schwierigkeit liegt in der Aufgabe an den richtigen Stellen zu unterteilen.
Keep it simple, stupid. Wenn es eine einfache und eine komplizierte Möglichkeit gibt, ein Problem zu lösen, nimm die einfache, auch, wenn sie noch so dumm erscheint.
Das klingt erstmal trivial und vollkommen einsichtig. In der Praxis sind aber gerade hier Abwägungen nötig.
Wenn es für ein Problem sowohl eine spezielle Lösung gibt, die nur dieses eine konkrete Problem löst, als auch eine allgemeine, die darüber hinaus noch weitere Probleme löst, so nimm die allgemeine. Verwende abstrakte Konzepte, die sich auf eine Vielzahl von konkreten Problemen anwenden lassen.
Auch das ist trivial und einleuchtend. Wenn man eine Funktion schreiben muss, die die Wurzel von 2 berechnet, so schreibt man besser eine Funktion, die allgemein Wurzeln ziehen kann. Das ist der Kernaspekt der Wiederverwendung. Dennoch ist es nicht immer einfach, passende Abstraktionen zu finden.
Don’t Repeat Yourself! Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. [ProgPrag]
In „The Pragmatic Programmer“ (übrigens ein sehr lesenswertes Buch) erheben Andrew Hunt und David Thomas (nicht ganz zu unrecht) das DRY-Prinzip zum universellen Prinzip in der Softwareentwicklung. Wenn beispielsweise die selbe Information in einem System in unterschiedlicher Repräsentation (z.B. Datenbankschema und Klassenstruktur) vorhanden sein muss, dann sollte nur genau eine dieser Repräsentationen die maßgebliche sein (Single Point of Truth) und alle anderen daraus automatisch(!) generiert werden. Effektiv ist das DRY-Prinzip ein Verbot stumpfsinniger Copy’n’Paste-Programmierung. Siehe auch DRY in Ward’s Wiki.
Prinzipien der Modularisierung
In einem Modul sollten alle teile stark gebunden sein; Module untereinander sollten jedoch nur schwach gekoppelt sein.
Bindung beschreibt, wie die Teile eines Moduls (beispielsweise die Methoden einer Klasse) miteinander zusammenhängen. Kopplung beschreibt hingegen die Abhängigkeiten zwischen Modulen. Eine starke Bindung bedeutet also, dass keine Methoden, die miteinander nichts zu tun haben, in einem Modul zusammengefasst werden sollen. Das macht Module leichter verständlich und leichter austauschbar. Die leichte Austauschbarkeit ist aber auch nur dann gegeben, wenn die Kopplung zwischen den Modulen möglichst gering ist. Eine geringe Kopplung sorgt zudem dafür, dass eventuelle Änderungen an einem Modul möglichst wenige Änderungen in anderen Modulen (Ripple Effects) nach sich zieht.
Hierarchische Strukturen sind besser als nicht-hierarchische. Baumstrukturen sind besser als DAGs; DAGs sind besser als allgemeine Graphen.
Hierarchische Strukturen sind leicht zu verstehen und bieten dennoch gute Möglichkeiten der Strukturierung. Wir denken und klassifizieren in hierarchischen Strukturen, also sollten wir auch unsere Software so aufbauen.
Abhängigkeiten zwischen Modulen (Packages, Klassen, Methoden, etc.) sollten immer azyklisch sein.
Insbesondere bedeutet das, dass bidirektionale Assoziationen (im UML-Diagramm die Doppelpfeile) potenziell problematisch sind. Zyklische Abhängigkeiten erzeugen starke Kopplungen, die man ja normalerweise vermeiden sollte. Mehr dazu in der Wikipedia und im original bei Robert C. Martin.
Objekte sind wie Abwasserleitungen. Ich will dass sie zuverlässig funktionieren, mit den Details will ich aber nichts zu tun haben.
Objekte sollten Informationen kapseln, d.h. Schnittstellen anbieten, über die man mit dem Objekt interagiert. Darüber hinaus sollte die Kapsel alle internen Details verstecken. Dadurch kann man leicht die Interna verändern, ohne dass sich andere Teile des Systeme ändern müssen. Und hier noch etwas über den Unterschied von Kapselung und Information Hiding.
Überlege bei der Modellierung, was sich vermutlich irgendwann mal ändern könnte. Das sollte dann in einem separaten Modul gekapselt werden.
Dieses Prinzip ist Grundlage vieler Design Patterns der Gang of Four und wird auch in deren Buch erwähnt (welches übrigens ebenfalls sehr lesenswert ist). Nur das, was auch gekapselt ist, lässt sich auch leicht verändern, austauschen und wiederverwenden. Deshalb ist es wichtig, dass alles, was sich ändert auch gekapselt ist.
Ein Modul sollte nur genau eine gar definierte und abgegrenzte Aufgabe haben.
Das Single Responsibility Principle sichert starke Bindung und lose Kopplung.
Weitere hilfreiche Daumenregeln
Sag einem Objekt, was es tun soll und entscheide nicht über seinen Kopf hinweg.
Der prinzipielle Gedanke der Objektorientierung ist ja, dass man die Funktionalität auf verschiedene Objekte verteilt und diese dann über „Nachrichten“ (technisch gesehen Methodenaufrufe) kommunizieren. Dabei hat jedes Objekt seine klar definierte Aufgabe. Wenn man jetzt aber nur Daten aus einem Objekt holt, diese Verarbeitet und wieder zurück schreibt, so ist das Objekt quasi ein toter Datenbehälter. Das ist die Vorgehensweise wie man sie bei prozeduralen Programmen findet. In der Objektorientierung sollte man Aufagben delegieren. „Objekt, mach mal!“ Wie es letztendlich seine Aufgabe erledigt, ist prinzipiell egal. Wichtig ist nur, dass die Aufgabe erledigt wird. In der OO sagen die Objekte quasi immer „Das kann ich alleine!“. Und dann sollte man sie auch alleine machen lassen.
Etwas mehr Text und die „Geschichte vom paper boy“ gibts in The Art of Enbugging.
Baue ein Objekt-Modell der Prozesse, die durch die Software automatisiert werden sollen. Objekte sollten vorrangig etwas sein und nur „nebenbei“ etwas tun.
Idee der OO ist es, quasi die „Realität“ bzw. das, was durch die Software gemacht werden soll, mit Objekten „nach zu bauen“ also quasi zu „simulieren“. Die Objekte bzw. deren Klassen sollten sich direkt aus der Problemdomäne ergeben. Möchte man beispielsweise Datenbankverbindungen verwalten, so ist „Datenbankverbindung“ eine Klasse und nicht etwa „Datenbankverbindungsverwaltung“. Auch wenn die Klasse natürlich nicht die Verbindung selbst ist, so repräsentiert sie diese dennoch. Von einem Konzeptionellen Standpunkt aus, ist die Klasse (bzw. deren Instanz) die Datenbankverbindung.
Solche Verwaltungs-, Manager- und -Controller-Klassen sind oft ein Anzeichen dafür, dass man prozedural denkt. Prozedurales Denken ist „Ich brauch jetzt etwas, das XY tut. Also mach ich daraus eine Prozedur.“ Wenn man bei dieser Denkweise bleibt und nur „Prozedur“ durch „Klasse“ ersetzt, wird das Resultat nicht wirklich objektorientiert sein. Zur objektorientierten Denkweise (und auch ein paar weiteren Daumenregeln) siehe Was die OOP-Tutorials verschweigen.
Delegation ist oft besser als Vererbung.
Vererbung wird oft überschätzt oder falsch eingesetzt. Vererbung bzw. Generalisierung/Spezialisierung ist wichtig, aber nur ein Werkzeug von vielen. Und man muss es richtig einsetzen. Auf der anderen Seite ist Delegation oft einfacher und flexibler, wird aber unterschätzt. Siehe hierzu auch das Kreis-Ellipse-Problem und Kaffeemaschinen und OOD.
Regeln abwägen
Hierzu habe ich schon in The Tradeoff Game was geschrieben.
//Update (02.04.12): Nummerierungsfehler korrigiert
Permalink
Permalink