Programmierung

Modulare Programmierung mit CONZEPT 16

Jeder erfahrene Softwareentwickler weiß, dass die Beherrschung der Komplexität das Hauptproblem bei der Entwicklung großer Softwaresysteme ist. In diesem Artikel möchte ich Ihnen einen Ansatz vorstellen, mit dem Sie dieser stetik wachsenden Komplexität Herr bleiben: Die Modulare Programmierung.

Die modulare Programmierung ist ein Programmierparadigma, das das Ziel hat, Software in logische und möglichst unabhängige Module zu gliedern. Ein Modul kapselt seine Daten in einer internen Datenstruktur nach außen ab. Über eine Schnittstelle ermöglicht es den abstrakten Zugriff auf seine Daten und seine Logik ohne Kenntnis der Datenstruktur.

Pro und Contra

Durch die Separierung ergeben sich mehrere Vorteile: Module können einzeln und weitestgehend unabhängig geplant, entwickelt, getestet und eingesetzt werden. Der wesentliche Vorteil besteht aber in der Wiederverwendbarkeit. Universelle Module können an mehreren Stellen im Programm eingesetzt werden, müssen aber nur einmal entwickelt werden. Die Abstraktion verhindert Programmierfehler, die beim direkten Zugriff auf die Daten leicht passieren können und ermöglicht eine spätere Anpassung der internen Datenstruktur des Moduls ohne die Schnittstelle zu beeinflussen.
Ein gewisser Mehraufwand bei der Entwicklung eines Moduls sollte einkalkuliert werden: Ein Modul muss eine definierte Schnittstelle implementieren, über die es mit anderen Komponenten verbunden werden und interagieren kann. Bei einer späteren Anpassung der Schnittstelle ist zu beachten, dass diese Verbindungen gegebenenfalls angepasst werden müssen.

CONZEPT 16

Mit den Mitteln von CONZEPT 16 lässt sich die Technik der modularen Programmierung elegant umsetzen. Am Beispiel eines Moduls “Rectangle”, das ein Rechteck repräsentiert, möchte ich im Folgenden veranschaulichen wie das aussehen kann.

Daten

Für die Speicherung der Daten eines Moduls kann im Prinzip jede in CONZEPT 16 verfügbare Datenstruktur herhalten. Zur besseren Handhabung empfehlen sich Datenstrukturen vom Datentyp Handle, wie Kontainerelemente (Cte), globale Variablenbereiche oder Datensatzpuffer, um über den Deskriptor mehrere Instanzen voneinander unterscheiden zu können.

Das Modul “Rectangle” verwendet einen globalen Variablenbereich, der allerdings nur für das Modul sichtbar ist. Somit ist der direkte Zugriff auf die Daten bereits unterbunden:

// ++++++++++++++++++++++++++++++++++++++++++++++
// + Daten                                      +
// ++++++++++++++++++++++++++++++++++++++++++++++

global Data
{
  gWidth                : int; // Breite
  gHeight               : int; // Höhe
}

Schnittstelle

Um den Zugriff auf die Daten dennoch zu ermöglichen muss eine Schnittstelle definiert werden. Die Schnittstelle besteht aus mehreren Funktionen die die Daten des Moduls verarbeiten.

Konstruktor

Zur Erzeugung einer Instanz auf ein Modul benötigt es eine Funktion zum Initialisieren der Daten, einen Konstruktor. Der Konstruktor erzeugt die Instanz, eventuell benötigte weitere Datenstrukturen und setzt deren Inhalt auf einen Initialwert.

Der Konstruktor des Moduls “Rectangle” bekommt die Abmessungen des zu erzeugenden Rechtecks übergeben. Er überprüft die Abmessungen, erzeugt eine Instanz und übernimmt die Werte in die Instanz. Sind die Werte gültig gibt die Funktion einen positiven Deskriptor der Instanz zurück, andernfalls einen negativen Fehlerwert:

// ++++++++++++++++++++++++++++++++++++++++++++++
// + Initialisieren (Konstruktor)               +
// ++++++++++++++++++++++++++++++++++++++++++++++

sub Init // public
(
  aWidth                : int; // Breite
  aHeight               : int; // Höhe
)
: handle;                      // Instanz oder Fehler

  local
  {
    tVar                : handle;
  }

{
  // Werte ungültig
  if (aWidth < 0 or aHeight < 0)
    return(_ErrValueInvalid);
  // Werte gültig
  else
  {
    // Variablenbereich allokieren
    tVar # VarAllocate(Data);

    gWidth # aWidth;
    gHeight # aHeight;

    return(tVar);
  }
}
Destruktor

Um eine Instanz wieder freizugeben kommt ebenfalls eine Funktion der Schnittstelle zum Einsatz, der Destruktor. Der Destruktor gibt die eventuell erzeugten Datenstrukturen und die Instanz wieder frei.

Der Destruktor des Moduls “Rectangle” muss lediglich die Instanz freigeben:

// ++++++++++++++++++++++++++++++++++++++++++++++
// + Terminieren (Destruktor)                   +
// ++++++++++++++++++++++++++++++++++++++++++++++

sub Term // public
(
  aVar                  : handle; // Instanz
)
{
  // Variablenbereich instanzieren
  VarInstance(Data, aVar);

  // Variablenbereich freigeben
  VarFree(Data);
}
Funktionen

Mit weiteren Funktionen der Schnittstelle kann auf die Daten des Moduls und seine Logik zugegriffen werden.

Das Modul “Rectangle” stellt Funktionen zur Berechnung der Fläche und des Umfangs zur Verfügung:

// ++++++++++++++++++++++++++++++++++++++++++++++
// + Fläche ermitteln                           +
// ++++++++++++++++++++++++++++++++++++++++++++++

sub AreaGet // public
(
  aVar                  : handle; // Instanz
)
: int;
{
  // Variablenbereich instanzieren
  VarInstance(Data, aVar);
  // Fläche zurückgeben
  return(gWidth * gHeight);
}

// ++++++++++++++++++++++++++++++++++++++++++++++
// + Umfang ermitteln                           +
// ++++++++++++++++++++++++++++++++++++++++++++++

sub PerimeterGet // public
(
  aVar                  : handle; // Instanz
)
: int;
{
  // Variablenbereich instanzieren
  VarInstance(Data, aVar);
  // Umfang zurückgeben
  return(2 * (gWidth + gHeight));
}

Verwendung

Wird das Modul in einer separaten Prozedur gespeichert kann es mit dem Prozedurnamen als Namensraum verwendet werden.

Ist das Modul “Rectangle” in einer gleichnamigen Prozedur gespeichert, könnte das wie folgend aussehen:

main

  local
  {
    tRectangle          : handle;
    tArea               : int;
    tPerimeter          : int;
  }

{
  // Instanz erzeugen
  tRectangle # Rectangle:Init(2, 5);
  if (tRectangle > 0)
  {
    // Fläche berechnen: 10
    tArea # tRectangle->Rectangle:AreaGet();
    // Umfang berechnen: 14
    tPerimeter # tRectangle->Rectangle:PerimeterGet();
    // Instanz freigeben
    tRectangle->Rectangle:Term();
  }
}

Fazit

Wenn Ihre Applikation und/oder Ihr Entwicklerteam eine gewisse Größe erreicht hat, haben Sie bestimmt schon einige Mal nach einer Methode gesucht, die Weiterentwicklung besser organisieren zu können. Mit der modularen Programmierung haben Sie die Möglichkeit Ihren Entwicklungsprozess zu optimieren, und das mit viele Vorteilen: Wiederverwendbarkeit, einfaches Warten und Testen, parallele Entwicklung an unterschiedlichen Modulen durch mehrere Entwickler.

Diese Technik ist nicht nur auf einfache Module wie das gezeigt Beispiel anwendbar, auch komplexere Funktionalitäten in Interaktion mit der Benutzeroberfläche und Ereignissen lassen sich realisieren.

Auch die bereits vor einiger Zeit vorgestellten Module basieren auf diesem Modell:

12 Kommentare

12 Kommentare “Modulare Programmierung mit CONZEPT 16”

  1. @Manfred: Ja, da hast du recht und ich habe diese Vorgehensweise jetzt auch in mein Reperoire aufgenommen.

    Was die Kapsleung betrifft: Da die C16-Prozeduren die genaue Spezifikation exportierter Funktionalität nicht unterstützen, erstelle ich z.B. für jedes Modul zwei Prozeduren, eine, die die eigentliche Funktionalität enthält und eine die nur die Exporte der Library enthält, dies können Konstanten, Funktionen oder eben globale Datenbereiche sein usw. Diese Export-Spezifikation wird für die Verwendung der Library dann eingebunden. Mit Hilfe von Metavariablen kann man z.B. steuern, wo die globale Struktur und andere Elemente verfügbar sein sollen. Mit diesem Design kann man auch komplexere Libraries, die aus vielen aufeinander aufbauenden und voneinander abgeleiteten Modulen aufgebaut sind, realisieren. Insgesamt erhöht es m. E. die Kapselung und macht die Vereinbarungen (die natürlich leider immernoch gebrochen werden können) explizit.

    Neben all dem schon Erwähnten wäre auch eine Inklusion unter Angabe eines Namespace-Präfix extrem nützlich, um Namenskonflikte beim Inkludieren effektiv handhaben zu können (ich denke an so etwas wie "@I:LIB.String as S" o.ä.).

  2. Die fehlende objektoriente Sprache ist wirklich das einzige Manko von Conzept16. 20 Jahre nach dem Durchbruch der objektorientierten Programmiersprachen ist das heute einfach Standard, ob in der Literatur oder bei Stellenausschreibungen. Selbst Skriptsprachen sind heute objektorientiert. Für uns als kleineres Softwareunternehmen wird es immer wichtiger Mitarbeiter gewinnen zu können. Zur Zeit suchen wir Programmierer, unsere Favoriten haben abgesagt auch mit dem Hinweis auf fehlende Objektorientierung.

    Dabei ist m.E. Conzept16 gar nicht so weit weg davon. Mit den Instanzen der globalen Datenbereiche und vielleicht einem PRIVATE SUB wäre ja schon mal ein Anfang gemacht. Natürlich sollte man sich dann auch Gedanken über Klassen, Vererbung und Polymorphismus machen …

  3. @Kilian: Ein "Globaler Datenbereich" ist ja nicht wirklich global im Sinne anderer Programmiersprachen. Wenn man beim init() den Handle übernimmt und alle Aufrufe über diesen Handle steuert (und auch jeweils die Instanz des globalen Bereichs lädt), dann ist die Arbeit meiner Meinung nach wesentlich näher am Objektorientierten als bei der Arbeit mit Cte-Objecten.

    Trotzdem bleibt natürlich leider die Freiwilligkeit, jeder Programmierer kann die Vereinbarung durchbrechen.

  4. Ja, du hast sicher recht, ich bezweifle nicht, dass dein Ansatz funktioniert.

    Ich habe tatsächlich bisher immer Cte-Objekte benutzt, wenn ich etwas ähnliches machen wollte. Wahrscheinlich wären die globalen Daten aber besser, weil etwas flexibler.

    Ich denke, da wir uns ja einig sind, dass der modulare Ansatz sinnvoll ist, sollte man ihn konkret unterstützen und die diesbezüglichen Möglichkeiten verbessern. Dazu wollte ich ein paar Vorschläge machen.

  5. Der "globale" Datenbereich ist nur für das Modul sichtbar, nicht für Prozeduren, die das Modul verwenden. Er dient nur als Speichermedium. Genausgut könnten Sie ein Cte-Objekt oder ein Memory-Objekt verwenden, um die Daten zu speichern.

    Dieser Ansatz soll zeigen, wie man mit aktuell verfügbaren Mitteln modulare Programmierung realisieren kann, auch ohne Objektorientierung und Co.

  6. Ich käme ehrlich gesagt ausserhalb von C16 nie auf die Idee, eine modulare Programmierweise mit dem Gebrauch von globalen Daten in Verbindung zu bringen. In meinen Augen ist das ein Gegensatz, denn globale Daten führen in der Regel sehr schnell dazu, dass Funktionen und Module eben gerade nicht wiederverwendbar sind, weil sie von einem komplizierten im entscheidenden Moment unbekannten und unerreichbaren globalen Status abhängen. Üblicherweise heisst es daher auch meistens: "Nutze globale Daten nur im Ausnahmefall!" Ich denke hier wird so etwas auch nur deshalb gemacht, weil es in C16 keine Objekte gibt.

    Diese fehlende Objektorientierung zusammen mit dem Umstand, dass ich in einem Modul (einer Library) gar nicht angeben kann, wie ich meine Schnittstelle tatsächlich ausgestalten möchte, welche Funktionen ich exportieren möchte und welche nicht (so dass ich die eigentliche Schnittstelle nur auf einer unsicheren Vereinbarungsbasis herstellen kann) und schliesslich das in der Sprache nicht vorhandene funktionale Paradigma (Funktionen als Daten, Möglichkeit anonymer Funktionen, Callback-orientierte Programmierung usw.) erschweren eine wirklich effektive modulare Programmierweise.

    Diese Konstellation und die allgegenwärtige Datenbank, die man auch als globalen Datenspeicher missbrauchen kann, dürften bei einem unerfahrenen Programmierer wohl meistens zum Gegenteil einer modularen Programmierweise führen.

    Der Grund dafür ist, schätze ich, dass C16 als RAD-Tool voll auf einfachste Zugänglichkeit sämtlicher Applikationsebenen und gerade nicht auf Kapselung und ähnliche Konzepte setzen muss. Trotzdem könnte man m. E. mit der Zeit einige der o.g. Punkte (OOP, funktionale Programmierung, Möglichkeit Funktionalität einer Library explizit zu exportieren) integrieren, damit diejenigen Nutzer, die so etwas nutzen wollen, dies auch können. Jeder dieser Punkte würde das Produkt in meinen Augen technologisch voran bringen.

  7. Einen gewisser Overhead wird durch die Kapselung tatsächlich verbraucht. Der fällt allerdings in der Regel kaum ins Gewicht. Für rechenintensive Operationen sollte die Implementierung der Schnittstelle aber so ausgelegt werden, dass möglichst wenige Funktionsaufrufe nötig sind, um die gewünschte Operation durchzuführen.
    Die Schnittstelle ist genauso "anfällig" für Fehler wie jede andere native Funktion die mit Deskriptoren arbeitet. Sie als Programmierer müssen dafür sorgen, dass die verwendeten Deskriptoren gültig sind, sonst tritt ein Fehler auf.

  8. Das erzeugt meines Erachtens einen großen Overhead an Prozeduraufrufen und ist auch noch sehr anfällig, wenn z.B.
    der globale Datenbereich nicht mehr oder noch nicht existiert, oder der Deskriptor verbogen wurde.

  9. @Manfred

    Die Datenbankzugriffe über eine Schnittstelle zu kapseln, halte ich auch für einen sehr empfehlenswerten Ansatz.
    Wir haben geplant den Debuger mittelfristig zu erweitern, u.a. um eine Möglichkeit zum Debuggen von Datensatzpuffern.

    @Robs
    Die RecList kann – wenn auch nur bedingt – auch in parametrisierbaren Modulen verwendet werden. Die anzuzeigende Tabelle, Selektion, Filter, anzuzeigende Spalten usw. können zur Laufzeit konfiguriert werden.
    Eine Möglichkeit die Verarbeitung der DataList zu beschleunigen, ist derzeit nicht in der Planung. Aber ein Ansatz wäre, dass man die DataList mit leeren Zeilen füllt, diese aber erst im ersten Ereignis EvtLstDataInit befüllt.

  10. Vielen Dank für die Ausführungen, Florian!
    Ich denke auch, dass der modulare Ansatz zwingend ist, um vernünftige Ergebnisse zu erzielen. Einen Nachteil sehe ich allerdings in der Performance. Die RecList ist sehr flink in der direkten Anzeige von Datensätzen einer Tabelle bzw einer Selektion – diese basieren aber immer auf einer Datei der Datenstruktur. Als modular verwendbare Alternative (sprich: losgelöst von der Datenstruktur) gibt es die DataList, die allerdings nicht die Schnelligkeit der RecList erreicht. Gibt es hier Ansätze, um diesem Nachteil zu begegnen?

  11. Wir verwenden ein vergleichbares Konzept schon länger sehr erfolgreich. Zusätzlich achten wir darauf, auch Datenbankzugriffe zu kapseln, also in einer Prozedur nur eine Datei der Struktur anzusprechen.

    Um das nicht nur organisatorisch umzusetzen, sondern zu "erzwingen", kann man mit Datensatzpuffern arbeiten, deren Handles wieder in dem abgeschirmten globalen Variablenbereich liegen. Da sich Datensatzpuffer aber noch nicht so einfach wie Dateien der Struktur debuggen lassen, ist das in komplexeren Aufgabenstellungen leider (noch?) sehr zeitaufwändig.

Kommentar abgeben