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).

  • 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.

Diagram
Figure 1. Datenstrom in Ansicht

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

Diagram

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


1. In Wirklichkeit gibt es davor meistens ein Research Phase, eine Analyse des Problems und der Ausgangslage, aber sobald ich einen konkreten Lösungsweg entwerfe beginne ich meistens mit einem Gedankenmodell
2. die Definition von einfach ist dabei, was ich als einafach und angenehm empfinde :)
3. Funktion ist hier ein breiter Begriff und kann beispielsweise auch cmd-line Programmaufrufe in einem bash Programm umfassen