Einführung
Wenn ich ein neues Softwareprodukt entwerfe (oder irgendeine Art von Process/Projekt etc) beginne ich fast immer mit einer Art Gedankenmodell [1].
Dieses lässt sich fast immer am besten als Grafik entwerfen. Im Allgemeinen folgt es einer Graphen-Struktur (denn fast immer werden strukturelle Verbindungen modelliert). Diese Vorgehensweise hilft nicht nur mir das Modell zu erstellen und zu verfeinern, sondern es ist meistens auch der beste Einstieg für andere um den grundsätzlichen Aufbau und die Funktionsweise des Projektes zu verstehen (Wenn ich mir ein Projekt anschaue bin ich immer dankbar solche Graphiken in der Dokumentation zu finden).
Von dieser Grafik leite ich dann den ersten Entwurf meiner Software ab. Im Laufe der Zeit entstehen aber einige Probleme:
-
Die Anforderungen an die Software verändern sich. Die Grafik (als Teil der Dokumentation) und die Software driften langsam auseinander.
-
Andere Programmierer arbeiten an dem Projekt mit. Sie verändern die Architektur und die Dokumentation wird irreführend und fehlerhaft. Manchmal geht auch das grundlegende Architekturprinzip kaputt und die Software wird komplexer und schwerer zu warten.
-
Durch Refaktoring werden Namen verändert. Diese Namen werden oft nicht (oder verspätet) in der Grafik aktualisiert.
Diese und weitere Probleme hängen fast immer mit Projektalterung und Komplexitätszuwachs zusammen. Eines der hervorstechenden Symptome (oder auch der Ursachen. Wahrscheinlich beides.) ist ein Auseinanderdriften von Dokumentation und Code.
Vorhandene Lösungsansätze
Um diesen Schwierigkeiten Herr zu werden gab es in der Vergangenheit einige Lösungsansätze:
- Graphische Programmiersprachen
-
In manchen Bereichen scheint dieser Ansatz ziemlich gut zu funktionieren (z.B. Audio,Video und Bildbearbeitung) doch es gibt auch Probleme.
-
Speziell sobald Programme komplexer werden verliert man sich oft in einem völlig unübersichtlichen Kontrollfluss (wer schonmal versucht hat auch nur halbwegs umfangreiche SPS Programme zu debuggen weis wovon ich rede).
-
Das Deutsch limit scheint eine ähnliche Beobachtung zu sein.
-
-
Versionskontrolle und der Vergleich zweier Versionstände wird zum Albtraum (damit wird auch Testen und Projektplanung schwieriger).
-
- UML Diagramme
-
Diese werden oft verwendet um daraus Quellcode zu generieren.
-
Wenn man den Quellcode dauerhaft nutzen will werden die Diagramme immer detailierter und damit unübersichtlicher.
-
Die Programme zum Erstellen der Diagramme sind oft selbst sehr komplex (oft ist es leichter die Sache im Quelltext zu beschreiben).
-
Sobald man den generierten Quelltext editiert können die Änderungen nur schwer wieder in die Diagramme eingepflegt werden.
-
Verschiedene Diagrammtypen modellieren überschneidende Teile des Codes.
-
- Diagramme aus Quellcode erzeugen
-
Diesen Ansatz wählt z.B. doxygen.
-
Das dreht den Arbeitsfluss um. Aber gerade zu Beginn des Projektes hat man am meisten von einem Diagramm.
-
Die generierten Diagramme helfen oft nicht beim Verstehen der Programmarchitektur da sie willkürlich angeordnet sind und alles mit in ein Diagramm hineinpacken (sie können wichtige nicht von unwichtigen Informationen unterscheiden).
-
Diese Probleme müssen nicht zwangsläufig mit dem Lösungsansatz zu tun haben aber bei den vorhandenen Tools sind sie zu beobachten.
Mein eigener Workflow
In meinem persönlichen Arbeitsfluss bevorzuge ich es Diagramme mit einfachen Werkzeugen zu erstellen [2] bei denen ich mich auf die visuellen Aspekte konzentrieren kann (z.B. Freihand, Inkscape → Svg, ditaa → ascii art, yed → graphml). Auf diese Weise kann man aus der resultierenden Grafik leicht die Funktionsweise der Software verstehen.
Übersicht
Ziele
Das vorliegende Projekt versucht einen neuen Ansatz zu finden um Software zu entwerfen und die Softwaredokumentation aktuell zu halten Dabei geht es von folgenden Annahmen aus:
-
Die Architektur ist ein grundlegendes Merkmal einer Software und sollte zu Beginn des Entwicklungsprozesses skizziert und dokumentiert werden.
-
Die Architektur eines Projektes darf geändert werden (und das muss man oftmals um neuen Anforderungen zu genügen oder zu wachsen) aber das sollte niemals unbemerkt oder aus Versehen geschehen.
-
Zu verscheidenen Zeiten (Phasen) und bei verschiedenen Aufgaben sind unterschiedliche Sichtweisen auf die Implementierung erforderlich (grafisch ist nicht generell besser als Text oder umgekehrt und es sind nicht immer die gleichen Diagramme erforderlich).
-
Die grundlegenden Konzepte einer Software sollten einfach darstellbar und erklärbar sein.
Architektur
Um die vorher erwähnten Probleme zu beheben und die soeben definierten Ziele
unter Berücksichtigung der hier gestellten Constraints zu erreichen bedient sich
ansicht
folgender Grundiedee: Es übersetzt sowohl die Diagramme als auch den
Quellcode in einen Graphen (mit definierten Einstiegspunkten) und vergleicht
diese Graphen auf strukturelle Unterschiede.
Damit ist ansicht
grundlegend ein Eingabe- und Ausgabefilter von
Strukturgraphen. Diese Architektur lässt sehr flexibele Arebitsflows zu.
Gleichzeitig kann die Dokumentation von Projekten signifikant verbessert werden.
Im folgenden werden wir die grundlegenden Komponenten und Anwendungsfälle behandeln:
-
Einlesen von Dateien (TODO link)
-
Generieren von Dateien (TODO link)
-
Vergleichen und Manipulieren von Graphen (TODO link)
Bedienung
Kommandozeilen Interface
TODO
Asciidoctrine Erweiterung
TODO
API
TODO link auf die API auf crates.io
Ein konkretes Beispiel für einen Workflow
TODO Aus dem handgeschriebenen Entwurf für Ansicht übernehmen.
Implementierung
Das Format vom internen AST
TODO
Unterstüzte Diagrammtypen
Dataflow Diagramme
Eines der nützlichsten Diagramme für mich sind Dataflow Diagramme. Sie zeigen den Fluss der Daten durch ein Programm. Dabei definieren sie
-
die Eingangsdaten
-
die Ausgangsdaten
-
die verarbeitenden Funktionsblöcke
Beispiele sind:
-
Callgraphen
-
Bild- und Audioverarbeitung wie die Node-Diagramme bei Blender, etc
-
Workflow Diagramme wie bei concourse, n8n etc (auf diese Art liessen sich auch z.B. Makefiles visualisieren)
Die Ein- und Ausgangsdaten können z.B. Links auf Dateiformate oder Strukturbeschreibungen der jeweiligen Sprache sein.
Die verarbeitenden Funktionsblöcke sind zumeist nur die Namen der verarbeitenden Funktion [3].
TODO
Komponenten Diagramme
TODO
Zustandsmaschinen
State-Maschines
State-Maschines sind ein sehr nützliches Werkzeug um das dynamische Verhalten eines Programmes bzw. einer Komponente zu beschreiben.
Unterschiedliche Möglichkeiten State Maschines zu generieren
Aus der gleichen State-Maschine kann man auf verschiedene Weise Quellcode generieren. Nehmen wir als Beispiel ein vereinfachtes Modell von einem Auto
Aus diesem Modell lassen sich in der gleichen Programmiersprache verschiedene Quelltexte generieren, die sich in ihrer Benutzung stark unterscheiden.
Die Übergänge werden als Enum codiert
Das ist die Form, welche einem sofort in den Sinn kommt, wenn man eine Statemaschine generieren will.
enum class event {
leave_car,
enter_car,
close,
open,
start,
stop
};
enum class state {
door_open,
standing,
driving
};
class CarStateMachine {
CarStateMachine() {
this->st = state::standing;
}
void receive_event(event ev) {
switch(ev) {
case leave_car:
...
break;
case enter_car:
...
break;
case close:
...
break;
case open:
if (this->st == state::standing) {
// user code
this->st = state::door_open;
} else {
// extra leer gelassen
}
break;
case start:
...
break;
case stop:
...
break;
}
}
private:
state st;
}
- Vorteile
- Nachteile
-
-
Eine einzige Funktion/Methode implementiert den gesamten Ablauf. Dadurch wird diese gross und schwer lesbar.
-
Die Parameterübergabe ist unübersichtlich. Entweder hat man gar keinen Parameter oder für alle Übergänge den Gleichen.
-
Die States sind nicht voneinander isoliert. Durch globale Variablen kann ein State den anderen verändern, ohne dass das modelliert wurde.
-
Überprüfungen finden zur Laufzeit statt und sind daher langsamer
-
Die Übergänge werden als Methoden codiert
enum class result {
ok,
wrong_state,
...
};
enum class state {
door_open,
standing,
driving
};
class CarStateMachine {
CarStateMachine() {
this->st = state::standing;
}
result leave_car() {
if (this->st != state::door_open) { return result::wrong_state; }
// user code
return result::ok;
}
result enter_car() {
if (this->st != state::door_open) { return result::wrong_state; }
// user code
return result::ok;
}
result close(key k) {
if (this->st != state::door_open) { return result::wrong_state; }
check_key(k);
// user code
this->st = state::standing;
return result::ok;
}
result open(key k) {
if (this->st != state::standing) { return result::wrong_state; }
check_key(k);
// user code
this->st = state::door_open;
return result::ok;
}
result start(key k) {
if (this->st != state::standing) { return result::wrong_state; }
check_key(k);
// user code
this->st = state::driving;
return result::ok;
}
result stop() {
if (this->st != state::driving) { return result::wrong_state; }
// user code
this->st = state::standing;
return result::ok;
}
private:
state st;
}
- Vorteile
-
-
Die einzelnen Übergänge haben eigene Funktionen und sind dadurch leichter lesbar
-
Parameter können individuell für jeden Übergang festgelegt werden. Das erhöht die Lesbarkeit und verringert die Fehleranfälligkeit (da der Compiler bereits verhindert, dass man falsche Parameter an Übergänge übergibt, wo dies nicht gestattet wurde).
-
- Nachteile
-
-
Die States sind nicht voneinander isoliert. Durch globale Variablen kann ein State den anderen verändern, ohne dass das modelliert wurde.
-
Überprüfungen finden zur Laufzeit statt und sind daher langsamer
-
Die Stati werden als Typen codiert
TODO
- Vorteile
-
-
Parameter können individuell für jeden Übergang festgelegt werden. Das erhöht die Lesbarkeit und verringert die Fehleranfälligkeit (da der Compiler bereits verhindert, dass man falsche Parameter an Übergänge übergibt, wo dies nicht gestattet wurde).
-
Das trifft auch auf Übergänge zu, welche von mehreren Stati definiert werden.
-
-
Es wird so viel wie möglich zur compile Zeit definiert. Dadurch ist die Implementierung sehr effizient.
-
Die States können voneinander isoliert werden. Das verhindert Fehler durch globale Variablen.
-
- Nachteile
-
-
Es erfordert anders über States zu denken
-
Sequenz-Diagramme
Sequenz Diagramme
Sequenz Diagramme kann man hervorragend einsetzen um Unit-Tests zu beschreiben. Das trifft im besonderen auf interaktive Programme zu, bei denen die Reaktion eines Teilnehmers von den vorigen Kommunikationsabläufen abhängt.
Ein und Ausgabeformate
PlantUML
TODO
ASCII-Art
TODO Übernehmen von korrigierter PlantUML Ausgabe. Wir wollen es sowohl als Ein- als auch als Ausgabeformat. Zudem soll es eine Möglichkeit geben von und zu Ditaa artigen Diagrammen zu konvertieren (diese sind im Grunde gleich wie die von PlantUML, verwenden aber Zeichen, welche bei allen Fonts verfügbar sind).
SVG
TODO Nur als Ausgabeformat
Sequenz Diagramme filtern
Oftmals ist es gut ein Sequenz-Diagramm zu haben, welches die Kommunikation zwischen allen Teilnehmern zeigt. Dadurch kann man sich einen guten Überblick über die Gesammtsituation verschaffen.
Auf der anderen Seite ist für die konkrete Implementierung eines einzelnen Teilnehmers nur die Kommunikation interessant, an der er selbst beteiligt ist. Das macht es ebenfalls leichter mögliche unterschiedliche Szenarien abzubilden ohne den jeweiligen Teilnehmer anpassen zu müssen (Denn oft ändert sich nur etwas in der Kommunikation zwischen den anderen Teilnehmern).
TODO Befehl definieren, bei dem man einen AST auf die Kommunikation mit einem einzelnen Teilnehmer filtern kann.
Arbeiten mit Quellcode
Erzeugen
TODO
Cpp
TODO Bei Cpp konzentrieren wir uns zunächst auf das doctest
Framework
Validieren
TODO
Einlesen von Dateinen
Das Programm kann verschiedene Datenströme (Datentypen) einlesen:
-
ASCII Diagramme. Diese müssen gewisse Konventionen einhalten, damit sie richtig geparsed werden können.
-
Graphml Dateien. Wir verwenden das yed Format um auch graphische Aspekte gut darstellen zu können.
-
Quelltext Dateien. Es muss ein Parser für jede unterstützte Sprache geschrieben werden.
-
AST Graph. Der Graph selber kann als JSON Tree eingelesen werden.
TODO links zu den entsprechenden Subüberschriften
Jedes dieser Formate fügt auch eigene Zusatzinformationen zum AST hinzu. SVG, Graphml und ASCII z.B. Informationen zur Position/Style in der graphischen Darstellung. Quellcode z.B. Informationen zur Datei, Zeile etc aus der der AST abgeleitet wurde.
ASCII Art
TODO
Graphml
TODO
Quelltext
TODO
Json AST
TODO
Generieren von Dateien
Aus einem AST Graph kann das Programm verschiedene Datenströme (Datentypen) erzeugen. Sind in dem AST zusätzliche Informationen, zur eigentlichen Logik, enthalten, so werden diese (wenn möglich) mit in den Ausgabestrom eingearbeitet. Sind z.B. Positions- oder Styleangaben im AST vorhanden so würden sie beim Erzeugen einer SVG Datei, einer PNG Datei, einer Graphml Datei oder eines ASCII Art Bildes berücksichtigt werden (Wenn sie fehlen legt das Programm die Position selbst fest). Sind Informationen über die Datei, Zeile etc vorhanden in denen etwas implementiert wurde so würde das beim Erzeugen einer SVG-Datei als Link eingebaut werden.
Die Ausgabeformate sind grundlegend die gleichen wie die Eingabeformate (Zwar muss nicht für jedes Eingabeformat auch ein Ausgabeformat implementiert werden (oder umgekehrt) aber prinzipiel ist das schon wünschenswert).
ASCII Art
TODO
TODO plantuml kann ASCII-Art generieren. Vielleicht können wir von ihnen etwas lernen
Graphml
TODO
Quelltext
TODO
Json AST
TODO
Bildformate SVG und PNG
TODO
Vergleichen & Manipulieren von Graphen
Das Programm kann mehrere Graphen übereinanderlegen, subtrahieren, die Intersektion errechnen usw. Zudem kann es erkennen, ob es zu Konflikten kam. Wenn man mehr als zwei AST Graphen übergibt kann es erkennen, was hinzugefügt, entfernt oder auf mehreren Seiten editiert wurde (nach dem Prinzip wie bei git mit base, theirs, mine). Zudem gibt es einige Befehle um Teile des AST nach bestimmten Kriterien auszufiltern oder Constraints auf bestimmten Kriterien zu setzen (Hier helfen bestimmt Graph query Funktionen, pattern matching, etc).
Da man den AST als JSON exportieren kann, diesen dann mit einem beliebigen Programm (in einer beliebigen Sprache) verändern kann und ihn danach wieder einlesen kann sind den Möglichkeiten keine Grenzen gesetzt.
Mehere Graphen vereinigen
TODO
Zwei Graphen vergleichen
TODO
Informationen in einem Graphen filtern
TODO
Build
TODO