N'Abend :-)
Mein Projekt wächst und wächst derzeit auf dem STM32 und ich komme etwas
ins Schwimmen, was die Übersichtlichkeit angeht.
Als Datenbasis habe ich einen recht großen Struct, wo alle möglichen
Daten wie temperatur, Feuchte, Druck, Minima und Maxima sowie die
letzten 48h Historie all dieser Werte reinwandern. Der Struct ist global
in main.h und in jedem Modul verfügbar durch extern Anweisungen.
Nahezu sämtliche Routinen verändern Daten in diesem Struct oder holen
sich welche da raus, zb fuer den Bildschirm.
UpdateData()
DrawTemperatur()
DrawFeuchte()
DrawHistory()
usw. Keine dieser Funktionen hat mehr Parameter. Sie werden zyklisch
aufgerufen in der Hautpschleife und triggern durch Zeitflags der RTC.
Nun tragen Parameter aber zur Kapselung von Daten bei und machen klar,
was eine Funktion verändert. Bei mir sieht man das nicht mehr, man muss
schon genau hinschauen. Bloss macht es wohl keinen Sinn eine globale
Variable als Parameter zu übergeben, weil die eh bekannt ist. Und noch
weniger den struct sogar durch mehrere Instanzen durch zu reichen, wenn
Subroutinen ihn auch brauchen.
C++ ist nicht so meins, da ist das besser organsiert.
Wie handhabt ihr das?
Christian J. schrieb:> Nun tragen Parameter aber zur Kapselung von Daten bei und machen klar,> was eine Funktion verändert.
Wenn eine Funktion immer auf denselben Wert zugreift, braucht man diesen
nicht als Parameter spezifizieren, sondern kann ihn fest in die Funktion
reinschreiben.
(Und wenn jetzt jemand schwätzt, man wäre aber doch viel flexibler für
die Zukunft, wenn man ihn trotzdem als immer gleichen Parameter
übergibt: Dan kann man ja dann in dieser Zukunft in der man die Funktion
wirklich mal mit 2 unterschiedlichen Werten arbeiten lassen will immer
noch machen.)
Ein Programm, das ein paar globale Variablen hat und eine Reihe von
Funktionen, die auf diese zugreifen, ist nichts anderes, als EINE
Instanz einer Klasse mit eben diesen Variablen als Klassenvariablen und
eben diesen Funktionen als Memberfunktionen.
Andere "Features" von C++ hat man damit aber nicht (wie Vererbung etc.).
Christian J. schrieb:> Nahezu sämtliche Routinen verändern Daten in diesem Struct
Warum verändert "DrawFeuchte" dein Struct? Und warum heißt die Routine
nicht "ZeichneFeuchte" oder "DrawHumidity"?
Ich bin ja OO-Fan was das betrifft, aber das willst du ja nicht. Ich
würde dieses Struct aufteilen. Wie genau ist die Kunst, aber das Ding
scheint ja ein "Gottobjekt" zu sein.
Michael B. schrieb:> Wenn eine Funktion immer auf denselben Wert zugreift, braucht man diesen> nicht als Parameter spezifizieren, sondern kann ihn fest in die Funktion> reinschreiben.
So richtig schön kann man das, nur am Rande erwähnt, mit IoC lösen. Ist
aber im uC-Feld wohl schon zu viel des guten.
Michael B. schrieb:> Ein Programm, das ein paar globale Variablen hat und eine Reihe von> Funktionen, die auf diese zugreifen, ist nichts anderes, als EINE> Instanz einer Klasse mit eben diesen Variablen als Klassenvariablen und> eben diesen Funktionen als Memberfunktionen.
Eine sehr weite Auslegung von OO :)
Christian J. schrieb:> Jan H. schrieb:>> Warum verändert "DrawFeuchte" dein Struct? Und warum heißt die Routine> nicht "ZeichneFeuchte" oder "DrawHumidity"?>> Häh? kopfkratz
Denglisch?
Christian J. schrieb:> Mein Projekt wächst und wächst derzeit auf dem STM32 und ich komme etwas> ins Schwimmen, was die Übersichtlichkeit angeht.>> Als Datenbasis habe ich einen recht großen Struct, wo alle möglichen> Daten wie temperatur, Feuchte, Druck, Minima und Maxima sowie die> letzten 48h Historie all dieser Werte reinwandern. Der Struct ist global> in main.h und in jedem Modul verfügbar durch extern Anweisungen.> [...]> Wie handhabt ihr das?
Du hast Dich leider in diese blöde Situation gebracht, weil Du schon
viel zu früh optimieren wolltest. "Premature optimization is the root of
all evil" schrieb Donald Knuth schon 1973, und "The Art Of UNIX
Programming" empfiehlt die Reihenfolge: "make it work, make it right,
make it fast". Die Optimierung ist immer der letzte Schritt bei der
Entwicklung eines Programms, nicht der erste.
Nach Deiner Beschreibung habe ich den Eindruck, daß Deine Struktur zu
groß und Deine Funktionen zu umfangreich geworden sind. Daß zudem so
viele der Funktionen auf ein und dieselbe Struktur zugreifen, macht die
Sache sicher nicht einfacher, aber wenn Du Dir nicht die Zeit dazu
nimmst, wird es in Zukunft immer schwieriger werden, damit umzugehen.
Zuletzt dürfte es Dein Programm nicht allzu sehr aufblähen, den
Funktionen Zeiger auf die Strukturen zu übergeben. Vermutlich wird das
Dein Problem mit der abhandengekommenen Übersichtlichkeit aber nicht
lösen, daher würde ich über eine Restrukturierung nachdenken -- und zwar
genau jetzt, denn je länger Du damit wartest, umso aufwändiger und
fehleranfälliger wird das.
Michael B. schrieb:> Christian J. schrieb:>> Nun tragen Parameter aber zur Kapselung von Daten bei und machen klar,>> was eine Funktion verändert.>> Wenn eine Funktion immer auf denselben Wert zugreift, braucht man diesen> nicht als Parameter spezifizieren, sondern kann ihn fest in die Funktion> reinschreiben.>> (Und wenn jetzt jemand schwätzt, man wäre aber doch viel flexibler für> die Zukunft, wenn man ihn trotzdem als immer gleichen Parameter> übergibt: Dan kann man ja dann in dieser Zukunft in der man die Funktion> wirklich mal mit 2 unterschiedlichen Werten arbeiten lassen will immer> noch machen.)
Letztlich haben Funktionen zwei Funktionen: einerseits sollen sie
mehrmals benötigten Code einkapseln, andererseits aber auch die
Übersichtlichkeit des Programms erhöhen. Zu letzterem Aspekt gehört auch
die Typsicherheit: wenn ich einer Funktion, die einen Zeiger auf "struct
foo" erwartet, einen Zeiger auf "struct bar" übergebe, dann meckert mein
Freund, der Compiler. Insofern dient es der Übersichtlichkeit und
Fehlerfreiheit, keine globalen Variablen und für die Übergabe Zeiger auf
lokale Strukturen zu benutzen. Dabei geht es dann nicht um Flexibilität,
sondern um Übersichtlichkeit und Klarheit.
Sheeva P. schrieb:> Die Optimierung ist immer der letzte Schritt bei der> Entwicklung eines Programms, nicht der erste.
"Ich habe ja nichts gegen Flüchtlinge, aber ...."
So würde ich da argumentieren, denn man muss sich vorher über den
Datenfluss in einem Programm klarwerden, bevor man loslegt. Nachher
lässt sich das nur schwer wieder ändern und man haut sich jede Menge
Fehler rein, muss alte Backups zurückspielen, weil plötzlich nichts geht
geht und mehr gehbar gemacht werden kann. (Inzwischen bin ich soweit
meinen Backup Server auf 15 Minuten eingestellt zu haben.)
Es gibt zwei Arten von Routinen:
1. die, die rechnen
2. die die zeichnen
Beide sind scharf gegeneinander abgegrenzt.
Was ist also an dem Konzept falsch? Ich pappe mal einen Source dran, nur
zur Demo.
Christian J. schrieb:> Nur zur Verdeutlichung:
Also ich sehe da drei "Arten" von Daten:
1. aktuelle Meßwerte
2. historische Meßwerte
3. akkumulierte Meßwerte
Das könnte man doch prima in Einzelteile strukturieren, etwa:
Sheeva P. schrieb:> Insofern dient es der Übersichtlichkeit und Fehlerfreiheit, keine> globalen Variablen und für die Übergabe Zeiger auf lokale Strukturen zu> benutzen. Dabei geht es dann nicht um Flexibilität, sondern um> Übersichtlichkeit und Klarheit.
Die Übersicht hast du offenbar verloren, klar ist dir nichts.
Wenn (von mehreren Funktionen) auf eine (oder mehrere) globale Variablen
zugegriffen wird, ohne Parameter, ohne struct Pointer, sondern so wie
Chrustian es hat,
dann ist das sehr klar und vor allem wird es vom Compiler sehr gut
typgeprüft.
Aber die Klarheit kann im Wahn der Überstrukturierung dir schon mal
abhanden kommen.
Sheeva P. schrieb:> Also ich sehe da drei "Arten" von Daten:>> 1. aktuelle Meßwerte> 2. historische Meßwerte> 3. akkumulierte Meßwerte>> Das könnte man doch prima in Einzelteile strukturieren, etwa:
Welchen Vorteil bringt außer dass die Zugriffszeilen immer länger werden
und viele Punkte haben? Der Compiler macht daraus sowieso eine einzige
Tabelle.
Christian J. schrieb:> Sheeva P. schrieb:>> Die Optimierung ist immer der letzte Schritt bei der>> Entwicklung eines Programms, nicht der erste.>> "Ich habe ja nichts gegen Flüchtlinge, aber ...."
Der Zusammenhang erschließt sich mir leider nicht, was möchtest Du mir
damit mitteilen, bitte?
> So würde ich da argumentieren, denn man muss sich vorher über den> Datenfluss in einem Programm klarwerden, bevor man loslegt.
Das ist natürlich immer sinnvoll, aber nicht zwingend notwendig. Es
reicht schon, das Programm sofort zu refaktorieren, wenn man bemerkt,
daß es sich in die falsche Richtung entwickelt. Und natürlich ist es
auch immer eine gute Idee, von einander unabhängige Programmteile von
einander unabhängig zu schreiben und mit klaren Schnittstellen zu
versehen -- um den Überblick zu behalten, und damit einem der Compiler
besser helfen kann.
> Nachher> lässt sich das nur schwer wieder ändern und man haut sich jede Menge> Fehler rein, muss alte Backups zurückspielen, weil plötzlich nichts geht> geht und mehr gehbar gemacht werden kann. (Inzwischen bin ich soweit> meinen Backup Server auf 15 Minuten eingestellt zu haben.)
Ein Versionskontrollsystem wie Git oder Subversion ist bei so etwas
extrem hilfreich.
> Es gibt zwei Arten von Routinen:>> 1. die, die rechnen> 2. die die zeichnen
Das sind die Teile "V" (Verarbeitung) und "A" (Ausgabe) des
EVA-Prinzips. Es fehlt "E", die Eingabe -- also Funktionen, die die
Daten von Deinen Sensoren (oder sonstwoher) einlesen.
Und wenn ich so auf Deine Datenstruktur schaue, müßten da noch viel mehr
Arten von Funktionen sein:
1. Funktionen, die die Daten einlesen
2. Funktionen, die sie in die Historie eintragen
3. Funktionen, die die Historie akkumulieren
4. Funktionen, die auf das Display zeichnen
Und ich persönlich würde die Funktionen zu 4. sogar noch einteilen in
4a. Funktionen, die die Überschrift und die Skalen etc., sowie 4b.
Funktionen, die die eigentlichen Datenplots auf das Display malen.
> Beide sind scharf gegeneinander abgegrenzt.
...und durch die gemeinsame fette Datenstruktur wieder eng und
untrennbar mit einander verheiratet.
> Was ist also an dem Konzept falsch?
Ebenjene enge Verheiratung; mit kleineren, spezialisierteren Strukturen
wäre die Angelegenheit deutlich übersichtlicher.
> Ich pappe mal einen Source dran, nur zur Demo.
Ohne da jetzt komplett durchgestiegen zu sein, sieht man den Funktionen
an, daß die Datenstruktur suboptimal ist. Und, sei mir bitte nicht böse,
das Verschieben Deiner History ab Zeile 44 ist wirklich grausam; das
kann man viel schöner mit einer lokalen static-Variable lösen.
Christian J. schrieb:> Sheeva P. schrieb:>> Also ich sehe da drei "Arten" von Daten:>>>> 1. aktuelle Meßwerte>> 2. historische Meßwerte>> 3. akkumulierte Meßwerte>>>> Das könnte man doch prima in Einzelteile strukturieren, etwa:>> Welchen Vorteil bringt außer dass die Zugriffszeilen immer länger werden> und viele Punkte haben? Der Compiler macht daraus sowieso eine einzige> Tabelle.
In Deinem Eröffnungsbeitrag hast Du Dich doch darüber beklagt, daß Dir
der Überblick verloren gegangen sei, und gefragt, wie andere das machen.
Oder habe ich das falsch verstanden?
Ich verstehe auch nicht, was Du gegen längere "Zugriffszei_l_en" oder
gegen Punkte hast, wenn sie Dir helfen, die Übersicht zu behalten. Und
warum man viele Punkte braucht, wenn man eine große Datenstruktur auf
mehrere kleinere verteilt, ist mir ebenfalls schleierhaft.
Kannst Du mir also bitte kurz einmal erklären, was Du mit Deinem Thread
eigentlich bezweckst? Vielen Dank.
MaWin schrieb:> Die Übersicht hast du offenbar verloren, klar ist dir nichts.>> Wenn (von mehreren Funktionen) auf eine (oder mehrere) globale Variablen> zugegriffen wird, ohne Parameter, ohne struct Pointer, sondern so wie> Chrustian es hat,>> dann ist das sehr klar und vor allem wird es vom Compiler sehr gut> typgeprüft.>> Aber die Klarheit kann im Wahn der Überstrukturierung dir schon mal> abhanden kommen.
Es ist schade, daß Du wieder nicht mehr zum Thema beitragen kannst als
eine dumme Pöbelei und einen erneuten Beweis, daß Du ein lausiger
Programmierer bist. Es gibt ja gute Gründe, warum globale Variablen von
professionellen Entwicklern als schlechter Stil betrachtet werden, warum
alle mir bekannten Standards von ihrem Gebrauch dringend abraten und
warum auch Kerninghan und Ritchie dabei ausdrücklich zur Vorsicht
mahnen.
Sheeva P. schrieb:> Kannst Du mir also bitte kurz einmal erklären, was Du mit Deinem Thread> eigentlich bezweckst? Vielen Dank.
Du hast mir schon viele Ansätze geliefert, keine Sorge. Und dass man das
"Rollen" auch mit einem Ringpuffer machen kann ohne darin wirklich etwas
zu rollen ist mir klar. Aber das Funktionieren war mir erst wichtiger.
Bei der Aktualisierung jede Minute mit 59s "CPU Stop" dazwischen spielt
sowas aber auch keine Rolle mehr, Zeit habe ich ohne Ende.
Eine Separierung vom Zeichnen statischer Elemente (Beschriftungen,
Rahmen) und dynamischen (Balken) kommt noch, morgen vielleicht. Ich habe
aber erst gerade die Balken wieder vereinfacht, vorher habe ich nur das
Delta vorne abgezogen oder drangesetzt was aber wegen der dyn.
Skalierung manchmal schiefging, nur bei schnellen Wechseln Sinn macht,
die flackerfei sein sollen und ich einen Knoten im Hirn hatte.
Also bei so nem kleinen Furzprogramm mit IoC und anderem Patterngewichse
anzukommen ist völliger overkill. Das lässt sich komplett klassisch
strukturiert erschlagen. Der Moster-struct ist hässlich ja, aber selbst
wenn dieser oder Teile davon global bleiben ist das oft übersichtlicher
und einfacher für Anfänger zu handeln als ständig Referenzen oder
kopierterte Werte wie wild herumzureichen. Gut, Performance spielt hier
keine Rolle aber der JEWEILIGE Autor soll seinen Code lesen und handeln
können und da ist nur ER der Massstab, wenn er mit anderen Paradigmen
nix anfangen kann wird dann auch nicht besser wenn er die krampfhaft
anwendet. Hier und da ein paar kleine Ändeurngen und das Ding läuft und
bleibt dennoch übersichtlich. Und git solltest du dir mal anschauen,
Versionskontrolle ist eine enorme Hilfe beim Ausprobieren und
"Zurückrudern" zu früheren stabilen Versionen.