Das Grundthema, was ich mit diesem Beitrag zu vermitteln versuche, ist:
Wie programmiert man auf dieser Ebene strukturiert und übersichtlich, also "aufgeräumt", daß man, wenn man 1 Jahr später in den Code geht, sofort sieht, was da los ist.
Das eigentlich zu programmierende Programm existiert ja bereits, die FIBU, nur da fehlt es an Struktur, Übersicht und kontrolliertem Bildschirmaufbau.
Der Code dieser Version ist "zu sehr" ergebnisorientiert programmiert worden, funktioniert, ist aber nicht gut strukturiert und nicht schön fürs Auge. Das soll nun nachgeholt werden. Da er aber nun läuft, hab ich die stabile (aber unschöne) vorhandene Plattform so gelassen und schreibe die neue Version nicht als Änderung, sondern von Grund auf neu. Tiefgreifende Änderungen vorhandener PRogramme sind fast sichere Programmkiller, von da her.
Der Aufbau geht jetzt umgekehrt: anstelle erstmal das logische Problem zu lösen (Datenverarbeitung), wird bei dieser, der 2. Version, erstmal der ganze FORMALE Kram programmiert, ohne eine "wirkliche" Programmzeile, die sich mit Datenverarbeitung beschäftigt.
Man kann natürlich fragen, warum ich nicht einfach ein Programm für 100 Euro kaufe, was das auch kann.
Meine Antwort: wenn man es selbst programmieren KANN, ist die Lösung immer besser, weil besser angepaßt als ein KAUFMICH-Programm, was selbstverständlich als eierlegende Wollmilchsau (KANN ALLES) ausgelegt sein muß.
Was hier herauskommt, ist eine Programmierung, die speziell auf die Bedürfnisse und Vorlieben des Anwenders ausgelegt ist (individuelle Programmierung).
Und wenn das Programmieren auch noch Spaß macht, mir macht das seit vielen Jahren schon Spaß, natürlich nicht 365 Tage im Jahr, es gibt eben so Phasen, warum nicht?
Zum Thema Strukturierung in ANSI-C hier zwei weitere Änderungen der formalen Programmbestandteile.
Erstmal stört allmählich die immer länger werdende #define Liste sowie die banalen Funktionen, welche den wachsenden Haufen von Variablen initialisieren, also sozusagen die "Putzfrauen"- oder "Hausmeister"-Funktionen. Die sind zwar wichtig, die wollen wir aber später in dem dem Bereich, der sich mit der eigentlichen Datenverarbeitung beschäftigt, nicht immer vor Augen haben.
Deshalb wird dieser ganze ellenlange Bereich (der ja später noch länger wird) nun ausgegliedert in eine HEADER- bzw. INCLUDE-Datei.
Jeder C-Compiler hat für das Speicherformat eine Funktion.
DIe System-Dateien wie <stdio.h> liegen in dem INCLUDE Verzeichnis des Compilers, unsere Datei MYHEADER.h legen wir in das Hauptverzeichnis des PRogramms und benennen sie:
mein_header.h
Diese Include-Datei mit dem ganzen Parameter-Mist sieht dann so aus:
#define BLACK 0
#define BLUE 1
#define GREEN 2
#define CYAN 3
...
#define ENTER 13
#define ESCAPE 27
#define BACKSPACE 8
#define PFEILOBEN 72
... stark gekürzt, sie ist endlos lang, also die #define-Anweisungen für Konstanten, gefolgt von den Forward-Deklarationen der Funktionen.
Die braucht man, damit jede Funktion darauf zugreifen kann, auch wenn sie in der Reihenfolge der Programmierung an der falschen Stelle (davor) steht.
void TextColor(int fontcolor,int backgroundcolor,HANDLE SCReen);
void ResizeConsole(short x, short y);
void gotoxy ( short x, short y );
void clrSCR();
... sowie die dritte Gruppe, die nicht über #define erklärt sind, sondern als konstante Variablen beim Programmstart initialisiert werden müssen:
char MENUbuf[MENUZ][MENUS+1];
char DIALbuf[DIALZ][DIALS+1];
char MTLGbuf[MTLGBUFLEN][MTLGS+1];
char MBOXbuf[MBOXZ][MBOXS+1];
char OLWbuf[OLWBUFLEN][OLWS+1];
... und die Initialisierungsroutinen:
void clrMENU(){int z;colorMENU();for (z=0;z<MENUZ;z++){gotoxy(MENUX,z+MENUY);puts(MENUblank);}gotoMENU();}
void clrDIAL(){int z;colorDIAL();for (z=0;z<DIALZ;z++){gotoxy(DIALX,z+DIALY);puts(DIALblank);}gotoDIAL();}
void clrMTLG(){int z;colorMTLG();for (z=0;z<MTLGZ;z++){gotoxy(MTLGX,z+MTLGY);puts(MTLGblank);}gotoMTLG();}
void clrMBOX(){int z;colorMBOX();for (z=0;z<MBOXZ;z++){gotoxy(MBOXX,z+MBOXY);puts(MBOXblank);}gotoMBOX();}
void clrOLW(){int z;colorOLW(); for (z=0;z<OLWZ;z++) {gotoxy(OLWX, z+OLWY); puts(OLWblank); }gotoOLW();}
void init_formatstrings()
{
int i;
for (i=0;i<=KON_BREITE;i++)SCRblank[i]=BLANK; SCRblank[KON_BREITE+1]=EOS;
strncpy(MENUblank,SCRblank,MENUS); MENUblank[MENUS]=EOS;
strncpy(DIALblank,SCRblank,DIALS); DIALblank[DIALS]=EOS;
strncpy(MTLGblank,SCRblank,MTLGS); MTLGblank[MTLGS]=EOS;
strncpy(MBOXblank,SCRblank,MBOXS); MBOXblank[MBOXS]=EOS;
strncpy(OLWblank,SCRblank,OLWS); OLWblank[OLWS]=EOS;
}
void init()
{
resetcolor();
ResizeConsole(KON_BREITE,KON_HOEHE);
clrSCR();
init_formatstrings();
clrMENU();
clrDIAL();
clrMTLG();
clrMBOX();
}
Damit haben wir einen Riesenhaufen Code, der nur der Initialisierung dient, aus dem Programmlisting raus und halten es dadurch übersichtlicher und lesbarer.
Damit das Programm diesen COde kennt, müssen wir nur eine einzige Zeile einfügen (anstelle der 3-n DINA4-Seiten, die wir umgelagert haben):
Dazu simulieren wir nicht den Aufruf der Standard-Bibliotheken a la
# include <MEIN_HEADER.h>
sondern wir formulieren:
# include "mein_header.h" bzw, wie hier:
#include "fibu2012.h"Der Compiler sucht dann im Stammverzeichnis, findet er dort die Datei nicht, geht er in das Systemverzeichnis mit den Systeminclude-Dateien, findet er dort auch nichts meckert er bei der ersten unbekannten Variable oder Funktion.
Die Auslagerung dieser ganzen Initialisierung- und selbstgeschriebenen Funktionen hat natürlich auch den Vorteil:
Wenn man eine andere Anwendung schreibt, kann man sie mit der #include Anweisung in die nächste Anwendung einbringen und braucht die dort versammelten Funktionen nicht mehr neu zu schreiben, sondern kann direkt drauf zugreifen.
Der nächste Punkt wäre,
REDUNDANZEN
abzubauen.
Redundanzen sind gleichlautende Code-Abschnitte. Die sind entstanden durch Copy und Paste und Ersetzen, und bei der Programmentwicklung ganz praktisch, eigentlich aber keine stabilen Bausteine, weil die Änderungen an dem einen Clone nicht automatisch auf den anderen Clone übergehen.
Nach einer Zeit, wo die Funktionen so ungefähr die Form haben, wie sie benötigt wird, kann man sie zusammenlegen.
Wir haben hier 2 ursprünglich identische Code-Teile, die sich nur in der Benennung unterscheiden. Sie verwalten die Textspeicher ihrer jeweiligen Bildschirmbereiche:
int putsDIAL(char nachricht[255],int spalte, int zeile)
{
int s;
int pruef = OK;
if (strlen(nachricht)+spalte>DIALS||zeile>DIALZ) return NOTOK;
for (s=0;s<strlen(nachricht);s++) DIALbuf[zeile][spalte+s]=nachricht[s];
DIALbuf[zeile][DIALS]=EOS;
return OK;
}
int putsMTLG(char nachricht[255],int spalte, int zeile)
{
int fehler=NEIN;
int s;
int pruef = OK;
int sp=spalte; // lokale Kopie
int len=strlen(nachricht); // strlen nur einmal aufrufen und Ergebnis ablegen
if (len>MTLGS||zeile>MTLGBUFLEN) return NOTOK; // zu breit btw. Bereichsüberschreitung
if (sp+len>MTLGS)
{
sp=0;
fehler=JA;
}// zu weit rechts platziert
for (s=0;s<len;s++) MTLGbuf[zeile][sp+s]=nachricht[s];
MTLGbuf[zeile][MTLGS]=EOS; // Stringendemarkierung explizit
if (fehler==JA)
{
MTLGbuf[zeile][0]=219; // Die BEstrafung: ASCII CODE 219
return NOTOK;
} // ASCII CODE 219 zur STrafe
return OK;
}
Die ursprünglich identischen Funktionen haben sich differenziert, nämlich durch das Scrolling. Die eine kann Scrollen, die andere nicht. Insgesamt gibt es 4 Bildschirmbereiche und einen Bereich für das Überblendfenster, also 5 Funktionen, von denen bislang 2 scrollen. Stellen wir uns vor, es kommen noch 3-4 Fensterbereiche dazu, dann gibt das einen Haufen Code, der im wesentlichen identisch ist, aber bei Änderungen sehr sensibel reagiert, wir müssen bei dem einen nur ein einziges Zeichen falsch schreiben, schon spinnt das Programm herum.
Legt man diese Code-Teile zu einer einzigen Funktion zusammen, hat man 2 Nachteile:
Erstens muß die Parameter-Llste länger werden, zweitens sinkt natürlich die Performance, möglicherweise nur ein winziges bißchen, weil wir nun den ursprünglichen Funktionsaufruf verzweigen müssen, bis wir den richtigen Adressaten erkannt haben.
Der Vorteil ist aber, daß alle Änderungen aller Fensterbereiche in dieser EINEN Funktion programmiert werden können, also ÜBERSICHT + STABILITÄT.
Statt 50 Sicherungen, die im Haus verteilt an irgendwelchen Plätzen untergracht sind, haben wir nur noch EINE EINZIGE ZENTRAL-Sicherung.
So denkt ja jeder Handwerker auch, das ist eigentlich logisches Denken und keine Programmierung.
Hier werden nun diese Funktionen zusammengelegt zu einer einzigen (eierlegenden Wollmilchsau? so schlimm ist es nicht
):
int my_puts(int screen,char text[255],int spalte, int zeile)
{
int s;
int textlen=strlen(text);
int max_sp;
int max_z;
if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGZ ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIZ ; }
else return NOTOK;
if (textlen>max_sp || zeile>max_z || textlen+spalte>max_sp) return NOTOK ;
switch (screen)
{
case DIAL:
for (s=0;s<textlen;s++) DIALbuf[zeile][spalte+s]=text[s];
break;
case MTLG:
for (s=0;s<textlen;s++) MTLGbuf[zeile][spalte+s]=text[s];
break;
case MENU:
for (s=0;s<textlen;s++) MENUbuf[zeile][spalte+s]=text[s];
break;
case MBOX:
for (s=0;s<textlen;s++) MBOXbuf[zeile][spalte+s]=text[s];
break;
case OLWI:
for (s=0;s<textlen;s++) OLWIbuf[zeile][spalte+s]=text[s];
break;
}
}
DIe Funktion puts() ist eine Standardfunktion, der Name ist reserviert, also my_puts()
Wir haben 1 Parameter mehr als zuvor, nämlich für den Bildschirmbereich. Dafür müssen wir den Namen der Funktion nicht erinnern, für alle gilt jetzt
my_puts anstelle von putsOLW, putsDIAL usf.
Im if/else if Teil sortiert die Funktion aus, welcher Bildschirmbereich gemeint ist. Und sucht sich die passenden Werte für die Bildschirmdarstellung heraus (Breite, Höhe).
Wenn die Anforderung nicht paßt, z. B. die Zeile zu groß ist (gibt es nicht) oder das Format nicht platziert werden kann, bricht die Funktion ab mit NOTOK.
Diese Prüfung (bereichsprüfung) ist erstmal nötig, damit hier nichts anbrennt.
Leider müssen wir dann mit dem SWITCH-OPERATOR nun nochmal unterscheiden, was zu tun ist. Der Performance-Nachteil ist hier aber quasi gleich NUll, weil nur eine Handvoll Datenzugriffe mehr erfolgen.
Ruft man das so auf, erhält man erstmal nicht das gewünschte, nämlich in den Scrolling-Fenstern wird nur eine Bildschirmgröße Text abgelegt (Abb. = screenshot)
Das ist logisch.
Zunächst ist ja für alle die Bufferlänge (der gespeicherte Bildschirmbereich) als darstellbare Höhe festgelegt.
Das ist hier:
if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGZ ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIZ ; }
else return NOTOK;
Um die Fenster jetzt scrollfähig zu machen, müssen wir mitteilen, daß zwei von denen einen längeren Textbuffer bekommen haben, nämlich so:
if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGBUFLEN ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIBUFLEN ; }
else return NOTOK;
Man sieht, die Zusammenlegung in einer Funktion macht die Struktur auch übersichtlicher, wir sehen auf einen Blick, wo Änderungen stattgefunden haben.
Nachdem wir das nachgetragen haben, läuft das Programm wieder wie zuvor.
Der nächste Schritt wäre nun, auch die anderen Clones zusammenzführen.
Dadurch wird der PRogrammcode kürzer und übersichtlicher.
Nochmal: bisher wurde keine einzige Zeile Programmcode geschrieben, der der eigentlichen Datenverarbeitung dient.
Bisher ist das alles nur formaler Code für Gestaltung und Verwaltung.
Der Beitrag wurde von sharky bearbeitet: 06.02.2012, 20:33 Uhr
Angehängte Datei(en)
zusammenlegen.jpg ( 99.11KB )
Anzahl der Downloads: 4