Programmarchitektur

In dieser Lektion werden wir uns ein wenig mit Architektur beschäftigen. Vielleicht hast Du Den Begriff „Software-Architekt“ schon einmal gehört, tatsächlich machen Software-Entwickler gerne Anleihen bei den Architekten. Architekten planen und bauen Häuser, Softwarearchitekten planen und bauen Software. In dieser Lektion werden wir uns also ein paar Gedanken darum machen, wie man ein Programm aufbaut, ohne sich die Steine dabei selbst in den Weg zu legen.

Ich bemühe mich diese Lektion verständlich aufzubauen, doch erwarte ich nicht, dass alles am Ende der Lektion verstanden ist. Die nachfolgenden beiden Themen (Projekte und Erweiterungen) gehören zusammen. Im Idealfall nimmst Du Dir einige Stunden Zeit, um alle drei Lektionen hintereinander zu lesen.

Was bedeutet Software-Architektur?

C wurde 1972 erfunden. Zu dieser Zeit war das Hauptproblem, dass man ein Programm überhaupt lauffähig bekommt. Übliche Programmiersprachen wie Fortran ermöglichten es Assembler schöner zu programmieren, aber im Prinzip unterscheidet sich Fortran und Assembler in dieser Zeit kaum. Entsprechend wurde C ebenfalls so programmiert, wie man es aus Assembler und Fortran kannte.

Mit der Zeit entwickelte man aus den neuen Möglichkeiten Techniken, die einem halfen, qualitativere Software herzustellen. Ein großer Schritt dabei war, dass man erkannte, dass es eigentlich nicht darum geht Quelltext zu schreiben der Daten verarbeitet. Stattdessen stand im Mittelpunkt Daten zu beschreiben und dazu Quelltext, der diese Daten modifiziert oder in eine andere Form überführt zu erstellen. Und genau diesen Schritt schauen wir uns in dieser Lektion genauer an. Im Prinzip ändert sich sich nur der Blickwinkel. Wenn man statt auf das Programm zu gucken, dem man passende Daten zu füttern gibt, stattdessen auf die Daten schaut und sich darauf konzentriert, für diese Daten Programme zu schreiben.

Daten kommen meist aus der Realität und damit meist bereits in irgendeiner Form organisiert. Nehmen wir einen Brief: Es gibt einen Absender, einen Empfänger, ein Datum, einen Betreff und eben den Inhalt. All diese Dinge finden wir in einem E-Mail-Programm wieder. Der Absender eines Briefes besitzt eine Adresse. Entsprechend bietet das Mailprogramm ein Adressbuch, man kann Briefe in Ordnern ablegen und so weiter. Alles genauso wie bei normalen Briefen. Die Daten und ihre Struktur wurden direkt auf den Computer übernommen.

Die eigentliche E-Mail stammt aus den frühen 1980er Jahren und ist selbst nur ein langer Textblock, der sich nach dem Programmiermethoden dieser Zeit richtet. Eher wie eine Urlaubs-Postkarte, auf der auf einem viereckigem Stück Pappe alles hingekritzelt wird, was man irgendwie zu sagen hat und für das man noch ein bisschen freie Fläche gefunden hat.

Wir kennen also aus der Realität bereits häufig, wie die Daten eigentlich gut strukturiert aussehen und wie man sie voneinander abgrenzen kann. Diese Abgrenzung von Daten lässt sich auch in den Quelltext übernehmen. Während man früher einen großen zusammenhängenden Datenblock als Ganzes behandelt hat und damit große, unförmige Programme geschrieben hat, so versucht man heute sich mit möglichst kleinen Datenbereichen zu beschäftigen und diese deutlich gegenüber anderen Datenbereichen abzugrenzen. So erhält man Quelltexte, die sich nur um kleine Probleme kümmern und um nichts anderes. Man kann zum Beispiel die Adresse unabhängig vom Brief beschreiben. Damit sind diese kleinen Einheiten im Gegensatz zu großen Programmabschnitten weniger anfällig für Seiteneffekte, zum Beispiel weil eine wiederholt verwendete Variable nicht korrekt initialisiert wird, weil in der Mail vielleicht zufällig das Datum vor dem Empfänger steht.

Diese Lektion ist ein erster Schritt zum Software-Architekten. Der wichtigste Punkt dabei ist, dass wir nicht einfach etwas Programmieren, sondern uns vorher Gedanken darüber machen, was wir beschreiben bevor wir es programmieren und planen, wie wir diese Daten strukturieren.

Wie programmiert man datenorientiert?

Nun, wir haben zuvor bei der Organisation von Strukturen uns das HTML-Fragment angesehen:

<html>
  <body background="white">
    <h1>proggen.org</h1>
    C-Tutorial
  </body>
</html>

Wir sehen, dass es es Knoten gibt, die ineinander angeordnet sind („h1“ liegt innerhalb von „body“, „proggen.org“ liegt innerhalb von „h1“) oder nebeneinander („h1“ liegt neben dem Text „C-Tutorial“) und dass die Parameter (background=„white“) einfach nur einem Parameternamen einem Wert zuweisen. Parameter können nicht ineinander geschachtelt werden. Abschließend brauchen wir natürlich noch den eigentlichen Text („proggen.org“ und „C-Tutorial“). Die entsprechenden C-Strukturen haben wir bereits im vorhergehenden Kapitel beschrieben.

Wir könnten nun einen Parser schreiben, welcher diesen HTML-Text in einem Stück frisst oder wir können uns damit beschäftigen, zwei unterschiedliche Probleme auch getrennt voneinander zu lösen. Da die 80er vorbei sind, betrachten wir unterschiedliche Probleme auch getrennt voneinander.

Schauen wir uns zunächst die Parameterdaten an. Ein Parameter besteht aus einem Namen und einem dazugehörigen Wert. Da es mehr als einen Parameter gibt, packen wir das ganze in eine Liste.

void parameter_delete( struct Parameter * toDelete )
{
  if( toDelete )
  {
    printf( "-Parameter: %s\n", toDelete->Name );
 
    free( toDelete->Name );
    free( toDelete->Value );
    free( toDelete );
  }
}
 
struct Parameter * parameter_newWithLength( char const * name, 
                                            unsigned int nameLength, 
                                            char const * value, 
                                            unsigned int valueLength )
{
  struct Parameter * result = malloc( sizeof( struct Parameter ) );
  printf( "+Parameter: %*s\n", nameLength, name );
 
  if( result )
  {
    result->Next  = NULL;
 
    result->Name  = malloc( 1 +  nameLength );
    result->Value = malloc( 1 + valueLength );
 
    if( result->Name && result->Value )
    {
      memcpy( result->Name,   name,  nameLength );
      result->Name[nameLength] = '\0';
      memcpy( result->Value, value, valueLength );
      result->Value[valueLength] = '\0';
 
      return result;
    }
 
    /* Parameter konnte nicht angelegt werden */
 
    parameter_delete( result );
  }
 
  return NULL;
}
 
struct Parameter * parameter_new( char const * name, 
                                  char const * value )
{
  return parameter_newWithLength( name, strlen( name ), value, strlen( value ) );
}

Im Prinzip ist das Konzept hiermit schon grundlegend genannt: Es gibt Funktionen, die eine Struktur aufbauen und wieder zerstören. Da es in C keine Überladung1) von Funktionen gibt, schreibe ich den Namen der Struktur vor die Bedeutung der Funktion. Eine neue struct Parameter erzeuge ich mit parameter_new().

Auch der Abriss muss von einem Software-Architekten geplant werden.

Zum Erzeugen von Datenstrukturen hat sich new und zum Abbau delete durchgesetzt. Hierbei wird nicht nur Speicher angefordert oder freigegeben, sondern auch die Member2) initialisiert und für sie Speicher besorgt bzw. freigegeben.

Genauso verfahren wir mit der Node:

struct Node
{
  struct Node      * Next;
 
  char             * Text;         // Falls es ein Textknoten, wie "proggen.org" ist.
 
  char             * Name;         // Name des Knotens
  struct Parameter * Parameter;    // Parameter wie bei <body ...>
  struct Node      * SubNodes;     // H1 ist der erste Subknoten von body.
};

Wir haben hier einen Zeiger auf Text und einen Zeiger auf den Namen der Node. Ein Text hat aber keinen Node-Namen und eine Node beinhaltet lediglich andere Nodes (z.B. Text-Nodes) und Parameter, hat aber keinen Text. Einer der beiden Zeiger ist also eigentlich überflüssig. Wir benutzen die beiden Zeiger, um eine Text-Node von einem normalen Knoten zu unterscheiden: Bei einer Textnode ist der Name NULL, bei einem Knoten setzen wir den Text auf NULL.

void node_delete( struct Node * toDelete )
{
  if( toDelete )
  {
    struct Parameter *tempP, *p = toDelete->Parameter;
    struct Node      *tempN, *n = toDelete->SubNodes;
 
    printf( "-%s: \"%s\"\n", ( toDelete->Name ) ? "Node" : "Text", 
                             ( toDelete->Name ) ? toDelete->Name : toDelete->Text );
 
    free( toDelete->Name );  // Falls Name vorhanden, dann löschen
    free( toDelete->Text );  // Falls Text vorhanden, dann löschen
 
    // Parameter löschen
    while( p )
    {
      tempP = p->Next;
      parameter_delete( p );
      p = tempP;
    }
 
    // Unterknoten löschen
    while( n )
    {
      tempN = n->Next;
      node_delete( n );     // Rekursiver Aufruf
      n = tempN;
    }
 
    // Diese Node löschen
    free( toDelete );
  }
}
 
struct Node * node_newWithLength( char const * name, unsigned int nameLength )
{
  struct Node * result = malloc( sizeof( struct Node ) );
 
  printf( "+Node: %*s\n", nameLength, name );
 
  if( result )
  {
    result->Name = malloc( 1 + nameLength );
    if( result )
    {
      memcpy( result->Name, name, nameLength );
      result->Name[nameLength] = '\0';
 
      result->Next      = NULL;
      result->Text      = NULL;
      result->Parameter = NULL;
      result->SubNodes  = NULL;
 
      return result;
    }
 
    node_delete( result );
  }
 
  return NULL;
}
 
struct Node * node_new( char const * name )
{
  return node_newWithLength( name, strlen( name ) );
}

Wir sehen, dass node_new() und node_delete() viel mehr tun, als nur Speicher beschaffen und wieder freigeben. node_delete löscht Unterknoten mit Hilfe eines rekursiven Aufrufs.

Mit diesen beiden Strukturen können wir bereits einen HTML-Code beschreiben, lediglich für den Text benötigen wir noch eine Funktion, um eine Text-Node zu erstellen. Hierfür nehmen wir eine leicht modifizierte Version der bisherigen Node:

struct Node * node_textWithLength( char const * text, unsigned int textLength )
{
  struct Node * result = malloc( sizeof( struct Node ) );
  printf( "+Text: %*s\n", textLength, text );
 
  if( result )
  {
    result->Text = malloc( 1 + textLength );
    if( result )
    {
      memcpy( result->Text, text, textLength );
      result->Text[textLength] = '\0';
 
      result->Next      = NULL;
      result->Name      = NULL;
      result->Parameter = NULL;
      result->SubNodes  = NULL;
 
      return result;
    }
 
    node_delete( result );
  }
 
  return NULL;
}
 
struct Node * node_text( char const * text )
{
  return node_textWithLength( text, strlen( text ) );
}

Um nun die Knoten füllen zu können, fügen wir noch zwei Funktionen dazu, die Parameter und Unterknoten in die einfach verkettete Liste einfügen:

void node_addParameter( struct Node * node, struct Parameter * parameter )
{
  if( node )
  {
    struct Parameter ** parameterPtr = &node->Parameter;
 
    /* Letzten Parameter finden */
    while( *parameterPtr )
      parameterPtr = &(*parameterPtr)->Next;
 
    *parameterPtr = parameter;
  }
}
 
void node_addSubNode( struct Node * node, struct Node * subnode )
{
  if( node )
  {
    struct Node ** nodePtr = &node->SubNodes;
 
    /* Letzte Node finden */
    while( *nodePtr )
      nodePtr = &(*nodePtr)->Next;
 
    *nodePtr = subnode;
  }
}

Und zum Schluss schauen wir uns das Hauptprogramm an:

int main( void )
{
  /* Variablendeklarationen */
 
  struct Node      * html;
  struct Node      * body;
  struct Parameter * par;
  struct Node      * text;
  struct Node      * headline;
 
  /* Programmteil */
 
  html     = node_new( "html" );
  body     = node_new( "body" );
 
  node_addSubNode( html, body );
 
  par      = parameter_new( "background", "white" );
 
  node_addParameter( body, par );
 
  text     = node_text( "proggen.org" );
  headline = node_new( "h1" );
 
  node_addSubNode( headline, text );
  node_addSubNode( body, headline );
 
  text = node_text( "C-Tutorial" );
  node_addSubNode( body, text );
 
  /* Datenstruktur fertig */
 
  node_delete( html );
 
  return 0;
}

Dies baut uns das oben beschriebene HTML-Dokument in C auf und haut sie danach direkt wieder in die Tonne. Wohlgemerkt, es handelt sich dabei nicht um den Text, sondern um die Organisation der Daten, die in dem kurzen Stück HTML-Code enthalten sind. Mir ist an dieser Stelle wichtig, dass man sich das Konzept verinnerlicht, dass die beiden hier entwickelten Datenstrukturen „Node“ und „Parameter“ recht gut voneinander getrennt sind. Um die Lektion hier nicht noch weiter zu verlängern, habe ich Text und Knoten in eine Struktur gequetscht, in meiner tatsächlichen Umsetzung besitzt der Text eine eigene Datenstruktur. Es gibt Funktionen von Node, die sich mit Parametern beschäftigen, aber nur dafür, um die Node zu modifizieren, nicht um Werte von Parameter zu verändern. Optimal wäre, wenn node_addParameter() eine Funktion parameter_setNext() aufrufen würde, statt Next selbst zu setzen, dann wäre die Grenze perfekt und scharf gezogen.

Aber auch so fällt wohl auf, dass die Datenstrukturen der Dreh- und Angelpunkt für alle Funktionen sind. Die Datenstrukturen kennen sich und die Funktionen sorgen dafür, dass der Anwender (also der Programmierer, der diese Funktionen anwendet indem er zum Beispiel die main()-Funktion geschrieben hat) sich um nichts weiter kümmern muss. Die Funktion node_delete() sorgt zum Beispiel dafür, dass der komplette Aufbau gelöscht wird, der hier an die zu löschende Node angehängt wurde.

Mit diesen Funktionen haben wir jetzt ein Werkzeug, um darauf aufzubauen. Allerdings möchte ich darauf hinweisen, dass die hier vorgestellten Funktionen so gewählt sind, dass sie den HTML-Code abbilden und wieder löschen. Im produktiven Einsatz würde man weitere Funktionen beschreiben, die die Datensätze verändern können, einzelne vorhandene Knoten entfernen oder an anderer Stelle wieder einfügen. Dies ist für diese Lektion allerdings nicht erforderlich, daher ist die hier gemachte Implementation nur darauf ausgelegt, die beschriebenen Datenstrukturen so einfach wie möglich zu verwalten.

Fazit der Lektion

Wichtig an dieser Stelle ist mir, dass einem bewusst wird, wie man Funktionen um Daten herum aufbauen kann und die Datenhaltung oftmals in kleinere Unterprobleme zerlegen kann, wie hier in Knoten und Parameter. Ich habe hier eine Menge Quelltext in diese Lektion gepackt, den man sich auch durchaus mal ansehen kann, insbesondere die node_addSubNode() und node_addParameter()-Funktionen enthalten ein Stilmittel mit Zeigern auf Zeiger, das so in vielen anderen „modernen“ Sprachen wie Java oder C# gar nicht mehr formuliert werden kann.

Wenn wir das Programm so ausführen, sehen wir lediglich an den Konstruktoren und Destruktoren, dass die Datenstrukturen aufgebaut werden. Der vollständigen, kompilierfähigen Quelltext am Stück, inkl. der Includes, findet sich hier: XML-Quelltext. Wir werden in der nächsten Lektion den Quelltext nun in ein richtiges Projekt verwandeln und anschließend erweitern.

Um Erfahrungen in der Handhabung mit etwas „größeren“ Quelltexten zu gewinnen, werden wir diese Funktionen in den kommenden Lektionen weiter ausbauen und somit die Entwicklung einer Software nachvollziehen.

1)
Funktionen, die sich nicht durch den Funktionsnamen voneinander unterscheiden, sondern durch die Typen der Argumente
2)
Variablen innerhalb einer Struktur