Mit der Unix-Philosophie bezeichnet man die Konzepte, die beim Erstellen des ursprünglichen Unix-Systems von den Entwicklern angewandt wurden. UNIX wurde als System für Experten entworfen. Für Leute, die wissen was sie tun und was sie wollen. Auf Einsteigerfreundlichkeit wurde keinen Wert gelegt. Die Benutzerschnittstelle zur Interaktion mit dem Betriebssystem ist die Shell.
Es gibt einige Konzepte, die allgemein unter die Unix-Philosophie fallen. Ich möchte in diesem Beitrag einige der wichtigsten herausgreifen um einen Einblick zu geben.
Make each program do one thing well.
Anstatt ein großes monolithisches Programm zu entwickeln sollte ein Programm genau eine Sache tun — und das richtig! Ein solches Programm hat viele Vorteile:
Programme die genau eine Sache tun können speziell für diese Aufgabe konzipiert werden.
Viele einzelne Programme, die genau eine Sache machen, können sehr einfach kombiniert werden! Aus der Kombination von vielen einzelnen Dingen ergibt sich leicht etwas neues, viel größeres. Wie in einem Werkzeugkasten ist es sehr einfach sich die richtigen Programme für eine Aufgabe zu suchen und diese passend zu kombinieren. All das ermöglicht einen hohen Grad an Abstraktion, der es ermöglicht sehr schnell und effizient ein Ziel zu erreichen.
Um etwa herauszufinden, wie viele verschiedene Dateien in einem Verzeichnis liegen und welchen Typ diese haben können folgende Kommandos in der Shell kombiniert werden: file -b * | sort | uniq -c
.
Pipes |
sind ein effizientes Konzept um die Programmausgabe direkt als Eingabe an ein anderes Programm weiterzuleiten. Das Programm, an das weitergeleitet wird, beginnt mit der Verarbeitung sobald eine Eingabe vorliegt.
Summiert man die Quelltexte der einzelnen Programme auf kommt man auf mehrere tausend Zeilen Quelltext, die in dieser einen Zeile verwendet werden. Jedes dieser Programme wurde für genau eine Aufgabe entworfen, über Jahre konstant verbessert, ausgebessert und angepasst.
Bei unixoiden Systemen zieht sich dieser Aufbau durch, so sind viele Benutzeroberflächen oft einfach nur grafische Frontends für Kommandozeilentools (diskutil/Festplattendienstprogramm unter Mac OS X oder tcpdump/Wireshark etwa).
Small programs are beautiful.
Programme sollten klein sein. Das macht es einfach sie zu entwickeln, zu pflegen und zu verstehen. Für neue Entwickler ist es leichter sich in das Programm einzuarbeiten, die Funktionalität des Quelltextes ist überschaubar. Die Komplexität eines Programms wird so klein gehalten. Es fällt leichter Fehler zu finden und zu beheben.
Ein weiterer Vorteil ist, dass kleine Programme ressourcenschonend sind: Sie können vom Betriebssystem schnell geladen werden, swapping und paging sind seltener nötig. Im Endeffekt ergibt sich so ein schnelleres Gesamtsystem.
Wie aber legt man fest was ein “kleines” Programm ist? Mike Gancarz gibt in seinem Buch “The UNIX Philosophy” einige Anhaltspunkte: Funktionsdeklarationen, die aufgrund von vielen Parametern umgebrochen werden müssen oder auftretende Probleme, die nicht mehr augenblicklich einem Kontext zugeordnet werden können sind Warnsignale. Strikt der Unix-Philosophie folgend sollte ls
keinen Flag zur Formatierung oder Sortierung der Ausgabe haben — hierfür gibt es andere Programme (column
, sort
, …).
Dieser Aufbau von vielen verschiedene kleinen Programmen die zusammenarbeiten, lässt sich dem Konzept “Divide & Conquer” zuordnen: Dadurch, dass große, komplexe Probleme in einfachere, kleine zerlegt werden wird es einfach diese einzeln zu lösen.
Avoid captive user interfaces.
Programme sollten so geschrieben sein, dass sie problemlos in einem Skript verwendet werden können. Die Schnittstelle muss für Maschinen entworfen sein — nicht für Menschen. Das bedeutet beispielsweise, dass Programme nach dem Aufruf nicht noch auf Benutzereingaben angewiesen sein sollten (“Sind Sie sicher, dass Sie …? y/n”), sondern nur über die Kanäle STDIN, STDOUT & STDERR kommunizieren sollten.
Das macht Automatisierung einfacher, es ist sehr einfach Programme miteinander zu verknüpfen, die eben nicht irgendwann auf eine Benutzereingabe warten und deswegen blocken. Das UNIX-Konzept macht es sehr einfach, Programme zu kombinieren.
Zum Vergleich: Befehle wie dir
erzeugen unter Windows-Systemen auf ein leeres Verzeichnis angewandt eine Ausgabe mit mehreren Zeilen — ohne relevanten Informationen. Diese Ausgabe weiterzuverarbeiten oder zu parsen ist nicht ohne weiteres machbar. Programme, die strikt der Unix-Philosophie folgen verhalten sich dagegen nach dem Grundsatz “Silence is golden”.
Build a prototype as soon as possible.
In der Unix-Philosophie nehmen Prototypen eine zentrale Rolle ein. Die Sichtweise ist hier, dass viele traditionelle Softwareentwicklungsprozesse durch Anforderungsanalyse, Spezifikationen, etc. darauf abzielen von Anfang an das ideale System zu den Anforderungen zu bauen. Oder anders ausgedrückt: Beim ersten Mal alles richtig zu machen.
Die Sichtweise der Unix-Philosophie ist hier, dass sich ein solches System nie in einem Zyklus bauen lässt. Systeme müssen verschiedene Phasen durchlaufen und konstant, iterativ verbessert werden um Anforderungen ideal zu erfüllen. In jeder Phase muss dazu gelernt werden. Die Alternative ist einen Prototypen zu bauen und diesen iterativ auszubauen. Da Programme klein und überschaubar sein sollen fällt der Prototypenbau leichter.
Automate everything
Aufgaben, die heute von Hand ausgeführt werden, obwohl sie auch das System übernehmen könnte, sind nüchtern betrachtet Zeitsverschwendung. Da unter unixoiden Systemen Programme darauf ausgelegt sind mit anderen Programmen zu interagieren — und nicht mit Menschen, können Abläufe beispielsweise mittels Shellskripten einfach automatisiert werden. In Shellskripten können Programme und Kommandos wie Funktionen verwendet werden. Die Ausgabe kann direkt in den Programmfluss integriert weren.
Fazit
Die wichtigsten Konzepte zusammengefasst: Dinge einfach halten, Komplexität vermeiden. Eine Sache richtig machen. Programme miteinander kombinieren.
Das war jetzt lediglich ein kleiner Einblick in die Unix-Philosophie. Es gibt noch deutlich mehr Konzepte und man kann die Ideen auch nicht nur auf Betriebssysteme anwenden. Für weitere Informationen kann ich folgende Quellen sehr empfehlen:
Natürlich gibt es auch Probleme eines solchen Systemaufbaus. Diese habe ich hier absichtlich nicht thematisiert. Wer sich hierfür interessiert sollte sich etwa zu Plan 9 oder Kernelproblemen informieren.
]]>Im Normalfall führt das zu einem Chaining von Callback. Im folgenden Beispiel sollen zwei Dateien geöffnet werden und ihr kompletter Inhalt zurückgegeben werden:
var size = 0; fs.readFile('/home/benjamin/file1', function(err, data) { if (!err) { // first file read size += data.length; fs.readFile('/home/benjamin/file2', function(err, data) { if (!err) { // second file read size += data.length; fs.writeFile('/home/benjamin/size', size, function(err) { if (!err) { // both file read and content written to file sys.log('done'); } }); } }); } });
Das Problem hierbei ist, dass zwei Aktionen, die eigentlich parallel ablaufen könnten, nämlich das lesen beider Dateien, nacheinander ablaufen müssen. Für einen parallelen Ablauf ist eine zusätzliche Koordination notwendig, da die Resultate beider Aufrufe in Verbindung stehen. Hierfür habe ich mich an den CyclicBarrier
Klassen von Java orientiert und eine einfache Klasse für die Koordination von asynchronen Callbacks geschrieben. Eine Barriere wird erzeugt unter Angabe der Anzahl von teilnehmenden Parties. Desweiteren können optional ein Callback für die Beendigung sowie ein optionaler Callback im Abbruchsfall registriert werden. Anschließend können die einzelnen Aufgaben der Parties angestoßen werden. Im Erfolgsfall müssen diese an der Barriere mit submit()
anzeigen, dass sie beendet sind. Durch abort()
kann auch ebenfalls ein Abbruch signalisiert werden. Eine Propagation der Ergebnisdaten lässt am besten durch eine externe Variable realisieren, auf den die Funktionen ebenfalls Zugriff haben.
Der Code des Beispielszenarios ändert sich dann wie folgt:
var size = 0; var b = new Barrier(2, function() { //Success callback fs.writeFile('/home/benjamin/size', size, function(err) { sys.log('done'); }); }, function() { //Aborted callback sys.log("aborted"); }); fs.readFile('/home/benjamin/file1', function(err, data) { if (err) { b.abort(); } else { size += data.length; b.submit(); } }); fs.readFile('/home/benjamin/file2', function(err, data) { if (err) { b.abort(); } else { size += data.length; b.submit(); } });
Wie im Vergleich zu erkennen ist, lässt sich die Ausführung deutlich beschleunigen. Ein paar Rahmenbedingungen gibt es jedoch. Zunächst dürfen die Teilaufgaben keine Abhängigkeiten untereinander besitzen. Dann darf ein Abbruch einer Teilaufgabe keine Auswirkung auf noch laufende Teilaufgaben besitzen, dessen Ergebnis später verworfen wird. Schließlich muss bei der Programmierung darauf geachtet werden, dass in jedem Fall eine Teilaufgabe mit submit()
oder abort()
terminiert, da ansonsten ein Lock entsteht.
Die Klasse ist relativ einfach:
/** * @class * * Creates a new barrier for the given amount of parties. * @param parties * @param barrierCallback * @param abortCallback * @return */ var Barrier = function(parties, barrierCallback, abortCallback) { this.parties = parties; this.barrierCallback = barrierCallback; this.abortCallback = abortCallback; this.running = true; this.count = 0; }; /** * Signals a completion of one of the parties. * @return */ Barrier.prototype.submit = function() { if (++this.count === this.parties && this.running) { this.barrierCallback(); } }; /** * Signals an abort by one of the parties. If not callback is passed, the default abort callback will be executed. * @param customAbortCallback Optional callback that should be executed due to the abort. * @return */ Barrier.prototype.abort = function(customAbortCallback) { if (this.running && customAbortCallback) { customAbortCallback(); } else if (this.running && this.abortCallback) { this.abortCallback(); } this.running = false; };
Klasse auf github: http://gist.github.com/464179
]]>In diesem Beispiel soll ein DFA konstruiert werden, der aus allen möglichen Zeichenfolgen bestehend aus Einsen und Nullen genau diejenigen akzeptiert, in denen die Teilworte 01 und 10 gleich oft vorkommen. Der nebenstehende Graph verdeutlicht die Funktionsweise des Automaten. Neben dem Startzustand (S) gibt es zwei gültige Endzustände (A,X) mit gleicher Anzahl von Vorkommen, sowie zwei Zustände (B,Y) die keinen gültigen Endzustand darstellen.
Möchte man nun eine solche Konstruktion implementieren, so muss man Zustände, Übergänge und gültige Kombinationen abbilden. Das State Pattern der GoF als Verhaltensmuster bietet sich bei objektorientierten Sprachen an, da es übersichtlicher, sauberer und erweiterbarer ist als Kaskaden von If-Abfragen oder Switch-Statements-Konstrukten. Die Grundidee hierbei ist, Zustände mit zugehörigen Übergängen in eigene Objekte auszulagern und dem zustandsbehafteten Objekt die entsprechende Referenz auf seinen aktuellen Zustand zu übergeben. Änderungen delegiert dieses zustandsbehaftete Objekt dann an sein aktuelles Zustandsobjekt, welches eventuell nach dem Übergang ersetzt wird. In Java bietet es sich an, die Zustände als Enum-Instanzen zu realisieren und diese ein Interface implementieren zu lassen, welches die Übergange definiert. Zusätzlich lassen sich die Zustände, also Enum-Instanzen, darauf hin abfragen, ob sie einen gülitgen finalen Zustand darstellen.
Die Automaton-Klasse stellt nun das eigentliche Objekt dar, was zustandsbehaftet ist, jedoch mit ausgelagerter Behandlung der Zustände:
Transitions.java – Auflistung der Übergänge als Interface
/** * Interface listing possible transitions to be implemented by states */ public interface Transitions { public void readZero(Automaton a); public void readOne(Automaton a); }
State.java – Zustände als Enum-Typen
/** * Enum type for states */ public enum State implements Transitions { /** * Initial State - S */ S { @Override public void readOne(Automaton a) { a.setState(X); } @Override public void readZero(Automaton a) { a.setState(A); } @Override public boolean isFinal() { return true; } }, /** * Final State - A */ A { @Override public void readOne(Automaton a) { a.setState(B); } @Override public void readZero(Automaton a) { a.setState(A); } @Override public boolean isFinal() { return true; } }, /** * State -B */ B { @Override public void readOne(Automaton a) { a.setState(B); } @Override public void readZero(Automaton a) { a.setState(A); } @Override public boolean isFinal() { return false; } }, /** * Final State - X */ X { @Override public void readOne(Automaton a) { a.setState(X); } @Override public void readZero(Automaton a) { a.setState(Y); } @Override public boolean isFinal() { return true; } }, /** * State -Y */ Y { @Override public void readOne(Automaton a) { a.setState(X); } @Override public void readZero(Automaton a) { a.setState(Y); } @Override public boolean isFinal() { return false; } }; /** * Is state final? * @return true if final */ abstract public boolean isFinal(); }
Automaton.java – Das zustandsbehaftete Objekt inklusive Test in der Main-Methode.
/** * Automaton class */ public class Automaton { //Initial state private State state = State.S; public void setState(State s) { this.state = s; } public void readZero() { //Delegate... state.readZero(this); } public void readOne() { //Delegate... state.readOne(this); } public boolean isInFinalState() { //Delegate... return state.isFinal(); } public static void main(String[] args) { Automaton a = new Automaton(); String s1 = "11010101"; for (char c : s1.toCharArray()) { switch (c) { case '1': a.readOne(); break; case '0': a.readZero(); break; } } System.out.println(a.isInFinalState()); //true... } }
Dieses Beispielprogramm mit einem DFA habe ich zum leichteren Verständis gewählt, natürlich lassen sich mit diesem Pattern weit aus komplexere Abläufe modellieren. So könnte der Automat zum Beispiel ein Buch-Objekt einer Bücherei sein und die Übergänge Aktionen wie das Ausleihen, Verlängeren oder Zurückgeben des Buches. Die Zustände als Enum-Typen würden dann auch mehr Geschäftslogik enthalten als das bloße Verändern das Zustands wie hier in diesem Beispiel.
Originalartikel: Zustandsautomat in Java mithilfe des State Patterns
]]>//Explizit synchronisierte lazy declaration Variante - ineffizient! public class MyClass { /** Feld für Singleton-Instanz */ private static MyClass instance; /** * Privater Konstruktor verhindert externe Instanzierung */ private MyClass() { } /** * Statische, synchronisierte Factory-Methode * @return Singleton-Instanz */ public synchronized static MyClass getInstance() { if (instance == null) { instance = new MyClass(); } return instance; } }
Eine Alternative wäre das double-checked locking, was allerdings in Java durch das Speichermodell nicht sicher ist und dringend vermieden werden sollte.
Empfehlerswerter bei Java ist also die eager creation, die zum Beispiel so aussehen könnte:
//einfache eager creation Variante - evenutell unnoetige Instanziierung! public class MyClass { /** Feld für Singleton-Instanz */ private static final MyClass instance = new MyClass(); /** * Privater Konstruktor verhindert externe Instanzierung */ private MyClass() { } /** * Statische Factory-Methode * @return Singleton-Instanz */ public static MyClass getInstance() { return instance; } }
Durch einen kleinen Trick kann man verhindern, dass die Instanz schon beim Laden der Klasse erzeugt wird. Hierfür wird eine innere, statische Holder-Klasse geschrieben, die die Instanz kapselt. Diese Varinate ist außerdem implizit threadsicher und deswegen empfehlenswert:
//elegante eager creation Variante - implizite Threadsicherheit public class MyClass { /** * Privater Konstruktor verhindert externe Instanzierung */ private MyClass() { } /** * Innere statische Holder-Klasse */ private static class Holder { /** Gekapselte Instanz */ private static final MyClass INSTANCE = new MyClass(); } /* * Statische Factory-Methode * @return Singleton-Instanz */ public static MyClass getInstance() { return Holder.INSTANCE; } }
Seit Java 5 besteht noch eine alternative Variante, die sich die Eigenschaften des Enum-Konstruktes zu eigen macht. Anders als bei C/C++ besitzen die Enum-Typen bei Java objektähnliche Eigenschaften. Somit lässt sich ein Enum als Singleton-Klasse missbrauchen. Dadurch verliert man zwar die Möglichkeit, davon weitere Klassen abzuleiten, dafür ist die Singleton-Instanz direkt serialisierbar.
//Variante mit Enum ab Java 5 public enum MyClass { /** Singleton-Instanz als Enum-Typ */ INSTANCE; /** * Konstruktor */ MyClass() { } }
Originalartikel: Das Singleton-Pattern in Java
]]>