Forum: Mikrocontroller und Digitale Elektronik Softwaredesign


von Nils (Gast)


Lesenswert?

Hallo Forum,

aktuell beschäftige ich mit dem Thema Unit Testing. Zuerst habe ich mich 
mit der Theorie beschäftig, Bücher gelesen und die enthaltenen Tutorials 
abgearbeitet und dann habe ich mir eine kleine Applikation geschrieben 
und diese dann mit Unit Tests getestet. Soweit alles ok. Nun wollte ich 
das ganze auf unsere produktive Softare anwenden. Ja ich weiss, dass 
viele Unit Tests für bestehende Software als nicht so sinnvoll ansehen, 
aber das soll jetzt nich das Thema sein.
Das Problem ist, dass wir in unseren Modulen immer einen Pointer auf 
eine Struktur übergeben, Rückgabetyp ist oftmals void. kurzes Beispiel. 
In der Hydraulik.c und .h werden alle Filter, Druckschalter und 
Temperatursensoren des Systems behandelt. Jetzt gibts so einen leichten 
OO Ansatz, bei dem eine Struktur erzeugt wird, in der alle Variablen 
stehen, die die Hydraulik betreffen. Ein typischer Prototyp sieht dann 
so aus:
1
 void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )

Somit muss ich zum Testen immer wissen, wie die Struktur Hydraulik 
aufgebaut ist. Daher habe ich mal einem Kollegen gesprochen und gefragt 
warum man die Schnittstelle nicht abändert:
1
uint8 u8_GetTemperatureHydraulicOil( TemperatureSensor *pt_Sensor )

Somit muss ich noch immer die Struktur vom TemperatureSensor kennen, 
aber ich hätte die Temperatur direkt als Rückgabewert. Außerdem kann ich 
so in der Funktion nicht auf die komplette Struktur zugreifen, sondern 
nur auf den Sensor. mehr brauche ich ja auch in der Funktion nicht.
Der Kollege argumentiert, dass mit der Übergabe von pt_Hydraulik immer 
nur eine Adresse übergeben muss und halt nicht x Übergabeparameter und 
man muss sich keine Gedanken über Schnittstellen machen. Klar ist das 
easy, da ich in jeder Funktion zugriff auf alles habe. Aber solche 
Schnittstellen würden man in einer Neuentwicklung nicht mehr 
definieren?!
Daher stellt sich mir nun die Frage, ob man sich nicht zuerst um das 
Thema Softwaredesign/-architektur kümmert und danach um Unit Tests.
Oder ist das mit der Übergabe des Pointers auf die komplette Struktur 
noch state of the art in der C Programmierung?

Danke für eure Meinungen im Voraus!

Gruß Nils

von HG (Gast)


Lesenswert?

Na ja, man könnte dann ja, statt die Struktur mit allen Daten zu 
übergeben, diese gleich als globale Variable anlegen und spart den 
Parameter ganz.

Das ist natürlich genau das was man nicht machen sollte, aus den schon 
erwähnten Gründen... man kann nicht sicher sein was die Prozedur aus der 
Struktur braucht und was sie ändert, unabhängig von der Nutzung in 
Unittests.

von A. S. (Gast)


Lesenswert?

Nils schrieb:
> Daher stellt sich mir nun die Frage, ob man sich nicht zuerst um das
> Thema Softwaredesign/-architektur kümmert und danach um Unit Tests.

Unit Tests sind vor allem ein Element des TDD, des TestDrivenDesigns. 
Das Design wird also von der Erstellung der zugehörigen Tests getrieben 
(Anforderungen sind eigentlich nur dann Anforderungen, wenn sie auch 
getestet werden können, und diesen Test kreiert man vorab).

> Oder ist das mit der Übergabe des Pointers auf die komplette Struktur
> noch state of the art in der C Programmierung?

Deine Beispiele sind zu dürftig, um auf gutes oder schlechtes Design zu 
schließen.

Die übergabe der kompletten Struktur entspricht dem Verhalten jeder 
Memberfunktion in OOP (auch die hat zugriff auf alles).

Nils schrieb:
> void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )
> u8_GetTemperatureHydraulicOil( TemperatureSensor *pt_Sensor )
Das sind 2 völlig unterschiedliche Aufgaben, die kaum austauschbar sind. 
die obere setzt (vermutlich) die aktuelle Temperatur als property des 
Objekts (ist aber schlecht benamt), die zweite fragt sie ab.

Unit-Tests führen zu einfachen (unbrauchbaren) Funktionen aus Sicht der 
Befürworter (Gegner)

von MaWin (Gast)


Lesenswert?

Die Struktur verhindert nicht den Unit-Test. Klar musst du sie beim 
Aufruf per header einbinden und die relevanten Werte befüllen, aber das 
musst du auch bei deinem ptSensor, nur einfachste Datentypen wie int 
hätten weniger overhead.

Gerade heute programmiert man so: der implizite self Parameter bei 
objektorientierter Programmierung ist im Endeffekt so ein Zeiger auf 
eine Struktur, auf die die Funktionen arbeiten.

Das Interface-Problem ist dort dasselbe. Lern also, damit umzugehen. Ob 
die Struktur bei der Hydraulikanwendung sinnvoll ist, möchte ich gar 
nicht bewerten.

von Sheeva P. (sheevaplug)


Lesenswert?

Nils schrieb:
> aktuell beschäftige ich mit dem Thema Unit Testing. Zuerst habe ich mich
> mit der Theorie beschäftig, Bücher gelesen und die enthaltenen Tutorials
> abgearbeitet und dann habe ich mir eine kleine Applikation geschrieben
> und diese dann mit Unit Tests getestet. Soweit alles ok. Nun wollte ich
> das ganze auf unsere produktive Softare anwenden. Ja ich weiss, dass
> viele Unit Tests für bestehende Software als nicht so sinnvoll ansehen,
> aber das soll jetzt nich das Thema sein.

Es ist oft nicht ganz einfach, Unittests für eine bestehende Software 
einzuführen, aber sinnvoll sind sie trotzdem -- auch ohne Test-Driven 
Design oder andere Entwicklungsmethoden. Gerade bei einer Software wie 
Eurer, die sich auf Nebeneffekte verläßt, sind Unittests sinnvoll.

> Das Problem ist, dass wir in unseren Modulen immer einen Pointer auf
> eine Struktur übergeben, Rückgabetyp ist oftmals void. kurzes Beispiel.
> In der Hydraulik.c und .h werden alle Filter, Druckschalter und
> Temperatursensoren des Systems behandelt. Jetzt gibts so einen leichten
> OO Ansatz, bei dem eine Struktur erzeugt wird, in der alle Variablen
> stehen, die die Hydraulik betreffen. Ein typischer Prototyp sieht dann
> so aus:
>
1
 void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )

Mit ist nicht klar, wo da ein OO-Ansatz versteckt sein sollte. Das ist 
einfache strukturierte Programmierung; von den Elementen von OO -- 
Datenkapselung, Polymorphie, Vererbung -- sehe ich da nichts.

Andererseits ist dieses Design nicht besonders elegant, weil es sich auf 
Nebeneffekte verläßt -- und eben keine Objekte nutzt, deren Zustand über 
definierte Schnittstellen verändert werden kann. Jede Funktion, die 
einen Hydraulic-Pointer übergeben bekommt, kann die Inhalte der 
referenzierten Datenstruktur nach Belieben manipulieren. Wenn die 
Datenstruktur häufig geändert werden muß, kann das jedoch schnell zu 
einem Albtraum werden.

Gerade hier wären Unittests sinnvoll, um die Nebeneffekte wenigstens auf 
korrekte Funktion zu testen. Dazu erstellt der Tester einen 
Hydraulic-Mock mit Testdaten, ruft die zu testende Funktion auf, und 
überprüft hinterher, ob die Funktion nur genau die gewünschten Daten in 
genau der gewünschten Weise verändert hat.

Obendrein verträgt sich eine solche Programmierung mit Nebeneffekten 
eher schlecht mit Multithreading und Interrupts, weil man im 
Zweifelsfall nie genau weiß, in welchem Zustand sich die Datenstruktur 
gerade befindet. Es erfordert einige Klimmzüge, sowas thread- oder 
interrupt-safe zu bauen.

Mir ist auch nicht klar, wie bei so einem Softwaredesign sinnvoll auf 
Fehlerzustände überprüft oder gar reagiert werden könnte. Schließlich 
kann es ja immer mal vorkommen, daß ein Sensor oder ein Aktor ausfallen, 
und insbesondere in der Hydraulik mit ihren großen Drücken und nicht 
selten auch hohen Temperaturen erscheint es mir essentiell, Fehler 
erkennen und angemessen darauf reagieren zu können. Wenn man schon ein 
Design wie das von Dir beschriebene verwendet, sollten die Funktionen 
nicht void, sondern besser einen Erfolgs- oder Fehlercode zurückgeben, 
und der muß natürlich dann auch im Programmcode abgeprüft und angemessen 
behandelt werden.

> Somit muss ich noch immer die Struktur vom TemperatureSensor kennen,
> aber ich hätte die Temperatur direkt als Rückgabewert.

Richtig. Und mit einem sauberen OO-Design gäbe es einen abstrakte 
Klasse, die allgemein einen Temperatursensor beschreibt, sowie für jeden 
in Frage kommenden Sensor eine konkrete Klasse, die von der abstrakten 
Klasse erbt und sich um das Lesen eines bestimmten Sensortyps kümmert.

> Der Kollege argumentiert, dass mit der Übergabe von pt_Hydraulik immer
> nur eine Adresse übergeben muss und halt nicht x Übergabeparameter und
> man muss sich keine Gedanken über Schnittstellen machen. Klar ist das
> easy, da ich in jeder Funktion zugriff auf alles habe.

Natürlich muß man sich Gedanken über die Schnittstelle machen. Woher 
will man denn sonst wissen, in welches Element der 
Hydraulic-Datenstruktur die gelesenen Daten geschrieben werden müssen?

> Daher stellt sich mir nun die Frage, ob man sich nicht zuerst um das
> Thema Softwaredesign/-architektur kümmert und danach um Unit Tests.

Wahrscheinlich wäre es noch besser, diese Themen gleichzeitig anzugehen.

> Oder ist das mit der Übergabe des Pointers auf die komplette Struktur
> noch state of the art in der C Programmierung?

Nein. Im Embedded-Umfeld mit seinen besonderen Gegebenheiten kann man so 
etwas schonmal machen, aber State-Of-The-Art war das noch nie. Wenn ich 
mich recht entsinne, warnen schon Kernighan und Ritchie vor 
Nebeneffekten.

von Jan K. (jan_k)


Lesenswert?

Verstehe ich nicht - C++ oder ähnliche machen das doch genau so, nur wie 
oben angedeutet implizit.
In der C struct oben stehen die "properties" und vielleicht sogar noch 
die "Methoden" als Funktionszeiger drin. Von der Struktur kann man 
mehrere erzeugen, das ist dann wie die Instanziierung eines Objektes in 
C++. Wo ist da der Unterschied?

Wie würden denn die angesprochenen Probleme (Interrupts, Thread 
Sicherheit etc) mit einer OOP Sprache angehen?

Ich würde denken, dass alle Methoden einer "Klasse" (eines Moduls oder 
Sourcefile) durchaus ihre properties ändern dürfen. Nur dürfen externe 
Module da nicht dran, dafür gibt es setter und getter, oder?

Danke :)

von Nils (Gast)


Lesenswert?

Jan K. schrieb:
> Ich würde denken, dass alle Methoden einer "Klasse" (eines Moduls oder
> Sourcefile) durchaus ihre properties ändern dürfen. Nur dürfen externe
> Module da nicht dran, dafür gibt es setter und getter, oder?

Ja genau.

Sheeva P. schrieb:
> Mit ist nicht klar, wo da ein OO-Ansatz versteckt sein sollte.

Wie beispielsweise in C++ hat jede Funktion Zugriff auf den this Zeiger, 
in diesem Falle ist es halt ein Pointer auf die Struktur Hydraulik.


Sheeva P. schrieb:
> Obendrein verträgt sich eine solche Programmierung mit Nebeneffekten
> eher schlecht mit Multithreading und Interrupts, weil man im
> Zweifelsfall nie genau weiß, in welchem Zustand sich die Datenstruktur
> gerade befindet. Es erfordert einige Klimmzüge, sowas thread- oder
> interrupt-safe zu bauen.

Dazu haben wir Mechanismen, dass nicht Thread A in eine Struktur 
schreibt, die auch von Thread B genutzt wird. Wie gesagt mir geht es 
auch erst einmal um die Schnittstellen.

von A. S. (Gast)


Lesenswert?

Hallo Nils, noch können wir weder was zum bisherigen System sagen, noch 
zu Deinen Vorstellungen.

void v_GetTemperatureHydraulicOil mit nur einer Struktur als Parameter 
ist in jedem Fall schlecht benamt, was sie tun soll wissen wir nicht. 
Und wenn das:

Nils schrieb:
> Struktur erzeugt wird, in der alle Variablen stehen, die die Hydraulik
> betreffen.

Bedeutet, dass es nur genau eine Instanz gi bt, die verarbeitet werden 
kann ... dann ist die Funktion und Architektur überhaupt nicht mehr 
sinnvoll zu raten.

von Nils (Gast)


Lesenswert?

Hallo Achim,


Achim S. schrieb:
> void v_GetTemperatureHydraulicOil mit nur einer Struktur als Parameter
> ist in jedem Fall schlecht benamt, was sie tun soll wissen wir nicht.

Die Funktion liest den Sensorrohwert ein und rechnet ihn in eine 
Temperatur um.


Achim S. schrieb:
> Bedeutet, dass es nur genau eine Instanz gi bt, die verarbeitet werden
> kann ... dann ist die Funktion und Architektur überhaupt nicht mehr
> sinnvoll zu raten.

Genau, es gibt jeweils eine Instanz in dem jeweiligen Modul, der Rest 
get über Get und Set Funktionen

von lalala (Gast)


Lesenswert?

Nils schrieb:
> Die Funktion liest den Sensorrohwert ein und rechnet ihn in eine
> Temperatur um.

und speichert ihn dann wo?

von Jan K. (jan_k)


Lesenswert?

Würde raten in der "property" bzw eben in der struct, wo sonst?

Der Name ist schlecht gewählt, denn es ist kein "Get". Es ist eher ein 
"CalculateTemperatureFromRawValue".

edit: Ob die ganze Geschichte bei nur einer Instanz überhaupt Sinn macht 
sei dahin gestellt. Bei Klassen, von denen es mehrere Objekte gleicher 
Funktionalität gibt geht es doch fast nicht anders.

: Bearbeitet durch User
von Jack (Gast)


Lesenswert?

Zu

> C++ oder ähnliche machen das doch genau so,

C++ ist C++, nicht C. Bei C erwartet man (Stichwort Wartbarkeit), dass 
Dinge so gemacht sind, wie man sie in C macht, nicht in C++. Leider kann 
man von den zwei Codezeilen nicht sagen, was wirklich in dem Code 
gemacht wird.

In C versteckt man die Implementierung einer struct mit dem 
Handle-Idiom. Wenn man in C schon die Implementierungsdetails einer 
Datenstruktur. Das Idiom hat x verschiedene Namen und es gibt diverse 
Möglichkeiten zur Implementierung. Bei den gezeigten zwei Zeilen Code 
ist es unmöglich zu sagen ob so ein Idiom ansatzweise verwendet wird.

Was man anhand der zwei Zeilen sagen kann, ist das irgendwas schief 
läuft. Stichwort Code Smell. Darauf deutet die Verwendung einer Form von 
Hungarian-Notation hin. Gerade in Verbindung mit (angeblicher) 
Objektorientiertheit ist HN fehl am Platz. Mit Polymorphie in der 
objektorientierten Programmierung will man gerade nicht so genau wissen, 
an was man eine Nachricht sendet (Methodenaufruf), sondern nur, dass die 
Nachricht verstanden wird.

Für die üblichen Schusseleien in C hilft ein Lint wesentlich besser als 
sich auf HN und manuelle Kontrolle zu verlassen.

Nochmal zum Handle-Idiom, wer es nicht kennt, eine Implementierung sieht 
grob so aus:
1
#ifndef THING_H
2
#define THING_H
3
//
4
// Öffentliche Schnittstelle thing.h
5
//
6
7
//
8
// Unvollständiger Typ, damit können Nutzer nicht in thing bzw.
9
// THING hineinsehen
10
//
11
struct thing;
12
typedef struct thing THING;
13
14
// Deklaration von Funktionen die auf THING arbeiten können
15
THING *thing_new(int arg);
16
void thing_write(THING *t, char *message); 
17
18
#endif
1
//
2
// thing.c
3
//
4
#include "thing.h"
5
6
//
7
// Private Definition von thing, nicht außerhalb von thing.c sichtbar
8
// 
9
struct thing {
10
   int size;
11
   char buf[256];
12
   int last_op;
13
};
14
15
//
16
// Öffentliche Funktionen
17
//
18
THING *thing_new(int arg) {
19
 ...
20
}
21
22
void thing_write(THING *t, char *message) {
23
 ...
24
}

Man beachte die verwendete Namenskonvention. Funktionen die mit thing_ 
beginnen arbeiten auf THING*. Statt
1
void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )
oder
1
u8_GetTemperatureHydraulicOil( TemperatureSensor *pt_Sensor )
hätte ich so etwas wie das Folgende erwartet
1
int hydraulic_oil_temp(HYDRAULIC *h) {
2
   TEMP_SENS* ts = hydraulic_oil_temp_sens(h);
3
   return temp_sens_temp(ts);
4
}
Über die Kurzschreibweise (temp statt temperature, h, ts, usw.) kann man 
sicher streiten, aber das da oben bildet für mich logisch ab, dass die 
Hydraulik einen Temperatursensor hat, der die Hydrauliköltemperatur 
misst. Sowohl die Hydraulik-, als auch die Sensor-Daten sind jeweils mit 
Handles  eingekapselt. HYDRAULIC enthält ein Feld, dass einen TEMP_SENS* 
enthält, der bei der Initialisierung einer HYDRAULIC struct korrekt 
initialisiert wird.

von Nils (Gast)


Lesenswert?

Jack schrieb:
> Gerade in Verbindung mit (angeblicher)
> Objektorientiertheit ist HN fehl am Platz.

Ich habe ja geschrieben, dass es einen leichten Ansatz an OO gibt, da 
man immer Zugriff auf die komplette Struktur hat. Viel weiter geht 
dieser Ansatz auch nicht und ist auch nich gewollt, da man ja 
schließlich in C programmiert. Daher wird auch bewusst HN verwendet, da 
es Polymorphie einfach nicht gibt.

Was es aber gibt ist ein Modul Temperatursensor, Drucksensor, 
Druckfilter, ... die dann wiederum auf Module wie DigitalInput oder 
AnalogInput zugreifen. Und von den Temperatursensorem, Drucksensoren 
gibt es dann mehrere "Instanzen" in der Hydraulik Struktur.


Jan K. schrieb:
> Würde raten in der "property" bzw eben in der struct, wo sonst?

Exakt

von Markus L. (rollerblade)


Lesenswert?

Nils schrieb:
> Jetzt gibts so einen leichten
> OO Ansatz, bei dem eine Struktur erzeugt wird, in der alle Variablen
> stehen, die die Hydraulik betreffen.
Sheeva P. schrieb:
> Mit ist nicht klar, wo da ein OO-Ansatz versteckt sein sollte. Das ist
> einfache strukturierte Programmierung;
Alle Hydraulic-Variablen in eine Struktur zu packen, ist weder OO noch 
strukturierte Programmierung sondern erzeugt ein lebloses Value-Object, 
das dem AllInOne-Anti-Pattern folgt. Einziger Unterschied zu globalen 
Variablen: nicht jeder darf alle Hydraulic-Variablen sehen, aber der, 
der eine sieht, sieht auch alle anderen, auch wenn er sie gar nicht 
braucht.
Sowas produziert Spaghetti-Code und viel Aufwand beim Verunittesten, 
weil bei jeder Funktion, die eine solche Struktur erhält, im Detail 
nachgesehen werden muß, was sie von der Struktur tatsächlich verwendet 
und was nicht. Dieses Wissen muß man dann auch ständig mit sich 
rumschleppen, will man verstehen, was die Anwendung macht, weil man ja 
nicht immer wieder im Code nachsehen will, was er tut. Es steht beim 
Funktionsaufruf ja nicht da.

Eine Getter-Funktion mit Return-Code void folgt auch einem Anti-Pattern. 
Eine Getter-Funktion hat einen Wert zurückzuliefern. Deshalb trägt sie 
das Präfix "Get". Tut sie es nicht, stiftet sie Verwirrung.

Der erste Schritt hin zur strukturierten Programmierung wäre, daß der 
Caller einer solchen Funktion diejenigen Variablenwerte aus dem 
Value-Object herausnimmt, die die Funktion tatsächlich benötigt, und nur 
diese ihr übergibt. Die Funktion wird dann einen Wert zurückliefern 
müssen, da sie ihr Ergenis in der Struktur nicht mehr ablegen kann.

Sind alle Funktionen, die diese Struktur übergeben bekommen, auf diese 
Weise refactored worden, kann man sie funktional zu echten Objekten 
gruppieren. Es entstehen Klassen mit Attributen und Methoden. Das wäre 
dann OO. Jede Klasse beinhaltet nur noch die Attribute, die sie 
tatsächlich braucht -> google nach "Separation of Concerns" und "LCOM4".
Unterstützt die Sprache kein OO, baut man Module.

von Jan K. (jan_k)


Lesenswert?

Markus L. schrieb:
> Eine Getter-Funktion mit Return-Code void folgt auch einem Anti-Pattern.
> Eine Getter-Funktion hat einen Wert zurückzuliefern. Deshalb trägt sie
> das Präfix "Get". Tut sie es nicht, stiftet sie Verwirrung.
>
Zustimmung.
> Der erste Schritt hin zur strukturierten Programmierung wäre, daß der
> Caller einer solchen Funktion diejenigen Variablenwerte aus dem
> Value-Object herausnimmt, die die Funktion tatsächlich benötigt, und nur
> diese ihr übergibt. Die Funktion wird dann einen Wert zurückliefern
> müssen, da sie ihr Ergenis in der Struktur nicht mehr ablegen kann.
>
Das funktioniert aber nur, wenn der caller die Struktur kennt, das ist 
bei extern sichtbaren Funktionen [per Header publiziertes Interface) 
eher nicht gewollt. Bei "privaten" Methoden sehe ich dein Argument 
natürlich ein. Aber wenn ich aus einem anderen Modul bspw die Temperatur 
des Hydrauliköls lesen möchte, und es mehrere "Instanzen" von 
Hydraulikzylindern oder Temperatursensoren gibt, muss ich zwangsläufig 
irgendeine Instanzvariable übergeben, z.B. eben den struct Zeiger, oder?
> Sind alle Funktionen, die diese Struktur übergeben bekommen, auf diese
> Weise refactored worden, kann man sie funktional zu echten Objekten
> gruppieren. Es entstehen Klassen mit Attributen und Methoden. Das wäre
> dann OO. Jede Klasse beinhaltet nur noch die Attribute, die sie
> tatsächlich braucht -> google nach "Separation of Concerns" und "LCOM4".
> Unterstützt die Sprache kein OO, baut man Module.
Aber die Attribute sind doch eben in der struct? Oder sind in der 
Struktur von oben auch Variablen, die die "Klasse" NICHT benötigt? Dann 
hab' ich das möglicherweise falsch verstanden. Ich gehe nicht von einem 
All in One god Objekt für alles Mögliche aus, sondern nur von 
"Instanz-Strukturen" für eine konkrete Klasse.

von Nils (Gast)


Lesenswert?

Jan K. schrieb:
> Aber die Attribute sind doch eben in der struct? Oder sind in der
> Struktur von oben auch Variablen, die die "Klasse" NICHT benötigt? Dann
> hab' ich das möglicherweise falsch verstanden. Ich gehe nicht von einem
> All in One god Objekt für alles Mögliche aus, sondern nur von
> "Instanz-Strukturen" für eine konkrete Klasse.

Es ist schon alles getrennt. So hat beispielsweise das Modul Bohren 
keine Werte der Hydraulik enthalten und umgekehrt. Wie gesagt dort sind 
die Schnittstellen dann über Get und Set definiert.

von A. S. (Gast)


Lesenswert?

Nils schrieb:
> Es ist schon alles getrennt. So hat beispielsweise das Modul Bohren
> keine Werte der Hydraulik enthalten und umgekehrt. Wie gesagt dort sind
> die Schnittstellen dann über Get und Set definiert.

Ich vermute, ein Modul (wie Hydraulik) besteht aus mehreren C-Dateien. 
Ansonsten wäre die Struktur und deren Übergabe sinnlos.

Und ich hoffe, das Deine Beispielfunktion auch wirklich im 
Hydraulik-Modul liegt, sonst wäre auch das Modul sinnlos.

Irgendwie fürchte ich aber (z.B. wegen dem Namen), dass es doch nicht so 
ist und Du auch keine näheren Einzelheiten oder Fragen hast.

Solange Du kein klitzekleines Codebeispiel hast (oder gar konkurierende 
Ansätze), ist es eine reine Fahrt ins Blaue.

von R. R. (elec-lisper)


Lesenswert?

Nils schrieb:
>
1
 void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )
>
> Somit muss ich zum Testen immer wissen, wie die Struktur Hydraulik
> aufgebaut ist. Daher habe ich mal einem Kollegen gesprochen und gefragt
> warum man die Schnittstelle nicht abändert:
>
1
uint8 u8_GetTemperatureHydraulicOil( TemperatureSensor *pt_Sensor 
2
> )
>
> Somit muss ich noch immer die Struktur vom TemperatureSensor kennen,
> aber ich hätte die Temperatur direkt als Rückgabewert. Außerdem kann ich
> so in der Funktion nicht auf die komplette Struktur zugreifen, sondern
> nur auf den Sensor. mehr brauche ich ja auch in der Funktion nicht.
> Der Kollege argumentiert, dass mit der Übergabe von pt_Hydraulik immer
> nur eine Adresse übergeben muss und halt nicht x Übergabeparameter und
> man muss sich keine Gedanken über Schnittstellen machen. Klar ist das
> easy, da ich in jeder Funktion zugriff auf alles habe. Aber solche
> Schnittstellen würden man in einer Neuentwicklung nicht mehr
> definieren?!
> Daher stellt sich mir nun die Frage, ob man sich nicht zuerst um das
> Thema Softwaredesign/-architektur kümmert und danach um Unit Tests.
> Oder ist das mit der Übergabe des Pointers auf die komplette Struktur
> noch state of the art in der C Programmierung?

Sinnvolle Unit-Tests kann man nur schreiben, wenn man überschaubare
Teile (sog Units) des Systems für Tests abkapseln kann. Klassische
Unit-Testing geht immer von 1 Klasse == 1 Unit aus. Für mich
persönlich ist eine Unit einfach ein abgekapseltes Modul mit einer
klar definierten Schnittstelle. Das kann man natürlich letztendlich
als eine Fassade in eine einzelne Klasse packen und dann hat man
wieder seine 1 Klasse == 1 Unit.

Wie auch immer, der Knackpunkt ist es eine "klar definierte
Schnittstelle" für eine klar abgegrenzte Funktionalität zu haben. Das
ist der Dreh und Angelpunkt, ohne das kannst du nur automatisierte
Ende-zu-Ende-Tests machen, die bestimmte Pfade des Gesamtsystems
testen. Oft reicht mir das auch aus, 100% Test-Coverage ist ein
Non-Goal für meine privaten Projekte und für die Projekte auf Arbeit
(da wird sowieso auf Zuruf entwickelt, Pflichtenheft sind 2 Zeilen die
von einem Verkäufer nieder gekritzelt wurden).

Ich finde u8_GetTemperatureHydraulicOil(TemperatureSensor) von der
Semantik her totalen unfug. Entweder man hat
u8_GetTemperature(TemperatureSensor) oder
v_GetHydraulicOilTemperature(Hydraulic).

Vielmehr sollte wenn überhaupt v_GetHydraulicOilTemperature(Hydraulic)
dann u8_GetTemperature(TemperatureSensor) verwenden um die
Temperaturen vom Sensor zu holen. Dann kannst du einen Unit-Test für
u8_GetTemperature(...) schreiben.

Ein Lackmustest für Code ist auch sich anzusehen wie lang die
Funktionen/Methoden sind. Wenn eine Funktion/Methode die 100-200
Zeilen (ohne guten Grund) klar übersteigt, dann ist mit der
Faktorierung der Funktionalität meist etwas Faul.

Softwaredesign und -Architektur und Tests spielen alle ineinander. Das
wird bei TDD auf die Spitze getrieben, aber bis dahin gibt es viele
Abstufungen.  Eine großartige Diskussion zu TDD findet sich auf
YouTube: https://www.youtube.com/watch?v=z9quxZsLcfo da unterhalten
sich ein paar Ikonen der modernen Softwareentwicklung darüber (Kent
Beck und Martin Fowler) über TDD. Da gehts auch darum, ob man alles
Mocken sollte oder nicht und ob TDD dem ganzen gut tut usw. usf. Der
Fazit war: Selbst die Experten wissen es nicht genau, aber alle waren
sich einig: Das wichtigste für nachhaltige Softwareentwicklung
automatisierte Tests sind - seien es nun Ende-Zu-Ende oder Unit-Tests
oder ein paar Scripte die gewisse Annahmen über das System prüfen.

von Jan K. (jan_k)


Lesenswert?

Achim S. schrieb:
> Ich vermute, ein Modul (wie Hydraulik) besteht aus mehreren C-Dateien.
> Ansonsten wäre die Struktur und deren Übergabe sinnlos.

Warum sinnlos? Annahme: Hydraulik als Modul besteht aus einer C Datei. 
Es gibt aber mehrere physikalische Zylinder mit unterschiedlichen 
Parametern (Größe, Gewicht, Druck, Temperatursensor, Encoder, was weiß 
ich). Alle Zylinder haben aber die gleiche Funktionalität. Also macht es 
doch Sinn, mehrere Instanzen zu erstellen, und zwar genau so, wie Jack 
das oben gezeigt hat. Es gibt einen "Konstruktor", der entweder die 
Struktur dynamisch erzeugt und per Pointer zurückgibt, oder die Struktur 
wird vom caller statisch [z.B. wenn keine dynamische Speicherverwaltung 
existiert] erzeugt  (dann muss aber die Struktur bekannt sein, eine 
Vorwärtsdeklaration reicht nicht aus) und per Referenz an den 
Konstruktor übergeben. Auf jeden Fall kennt der caller den Zeiger auf 
die Instanzstruktur(en). Diese können jetzt an ein und dieselbe Funktion 
übergeben werden, z.B. an "GetHydraulikTemperature(hydraulik_t *this)". 
Allerdings sollte die Funktion dann doch auch etwas zurückgeben ;)

Ich bin nicht der TO, nicht verwechseln, aber mich interessiert das 
Thema.

Insbesondere interessiert mich dann auch, wie C++ Module getestet werden 
können, wenn das mit solchen Instanzzeigern so schwierig sein soll - 
immerhin hat eine echte Klasse in C++ ebenfalls Zugriff auf alle Klassen 
Attribute und Methoden...

Schöne Grüße

von A. S. (Gast)


Lesenswert?

Jan K. schrieb:
> Warum sinnlos

Sinnlos nur beim Programm des TE, der sagte, es gibt nur genau eine 
Struktur, also einen Kontext. Bei einer C Datei bräuchte es dann nur 
eines statcs.

von Stefan (Gast)


Lesenswert?

Nils schrieb:
> void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )
> Somit muss ich zum Testen immer wissen, wie die Struktur Hydraulik
> aufgebaut ist. Daher habe ich mal einem Kollegen gesprochen und gefragt
> warum man die Schnittstelle nicht abändert:uint8
> u8_GetTemperatureHydraulicOil( TemperatureSensor *pt_Sensor )
> Somit muss ich noch immer die Struktur vom TemperatureSensor kennen,
> aber ich hätte die Temperatur direkt als Rückgabewert. Außerdem kann ich
> so in der Funktion nicht auf die komplette Struktur zugreifen, sondern
> nur auf den Sensor. mehr brauche ich ja auch in der Funktion nicht.
> Der Kollege argumentiert, dass mit der Übergabe von pt_Hydraulik immer
> nur eine Adresse übergeben muss und halt nicht x Übergabeparameter und
> man muss sich keine Gedanken über Schnittstellen machen. Klar ist das
> easy, da ich in jeder Funktion zugriff auf alles habe. Aber solche
> Schnittstellen würden man in einer Neuentwicklung nicht mehr
> definieren?!
> Daher stellt sich mir nun die Frage, ob man sich nicht zuerst um das
> Thema Softwaredesign/-architektur kümmert und danach um Unit Tests.
> Oder ist das mit der Übergabe des Pointers auf die komplette Struktur
> noch state of the art in der C Programmierung?

um möglicht wenig impact im code zu haben :
- mach alle member der struct private
- implementiere setter und getter für jeden member
struct ist ja nix anderes als class.

als Rückgabewert ist imho immer this erste Wahl.
printf ( ... ,  doWhatever( struct*,changeThat,andThat ).getWhatEver());

Stefan

von Sheeva P. (sheevaplug)


Lesenswert?

Nils schrieb:
> Sheeva P. schrieb:
>> Mit ist nicht klar, wo da ein OO-Ansatz versteckt sein sollte.
>
> Wie beispielsweise in C++ hat jede Funktion Zugriff auf den this Zeiger,
> in diesem Falle ist es halt ein Pointer auf die Struktur Hydraulik.

Unser beider Verständnis von OO scheint sich erheblich zu unterscheiden. 
Ein OO-Ansatz würde die Subsysteme eines Hydraulikzylinders (etwa die 
Druck- und Temperatursensoren, Ansteuerventile) jeweils als Objekt und 
aus den Objekten dann einen Hydraulikzylinder modellieren. Aber lassen 
wir das, hier geht es ja nicht um OO.

> Dazu haben wir Mechanismen, dass nicht Thread A in eine Struktur
> schreibt, die auch von Thread B genutzt wird. Wie gesagt mir geht es
> auch erst einmal um die Schnittstellen.

Nunja, Du hattest auch gefragt, ob Du lieber gleich mit der Einführung 
von Unittests beginnen oder erst ein Refactoring durchführen solltest. 
Meiner persönlichen Ansicht nach wäre allerdings ohnehin ein Refactoring 
geboten, und wenn Du schon dabei bist, kannst Du dabei auch gleich 
Unittests bauen. Das hilft zunächst beim Refactoring, und dann 
langfristig auch bei Wartung und Weiterentwicklung der Software.

von A. S. (Gast)


Lesenswert?

> um möglicht wenig impact im code zu haben :
> - mach alle member der struct private
> - implementiere setter und getter für jeden member
> struct ist ja nix anderes als class.
>
> als Rückgabewert ist imho immer this erste Wahl.
> printf ( ... ,  doWhatever( struct*,changeThat,andThat ).getWhatEver());
>
> Stefan

ich hoffe Du hast das Ironie-Flag vergessen. Oder beziehst Dich auf 
einen anderen Thread. Oder wolltest beschreiben, was bei C++ möglich 
wäre ....

von Nils (Gast)


Lesenswert?

Jan K. schrieb:
> Warum sinnlos? Annahme: Hydraulik als Modul besteht aus einer C Datei.
> Es gibt aber mehrere physikalische Zylinder mit unterschiedlichen
> Parametern (Größe, Gewicht, Druck, Temperatursensor, Encoder, was weiß
> ich).

Nochmal kurz zum Aufbau der Software. Also es gibt die Hydraulik.h


1
typedef enum
2
{
3
hydNIL,
4
hydError,
5
hydInitialized
6
}Hydraulic_State_E;
7
8
9
10
typedef enum
11
{
12
.
13
.
14
.
15
} Hydraulic_Filter_State_E;
16
17
18
19
typedef struct
20
{
21
uint16_t u16_Channel;
22
uint8_t u8_Temperature;
23
.
24
.
25
} Hydraulic_Temperature_Sensor_T;
26
27
28
29
typedef struct
30
{
31
uint16_t u16_TimeToError;
32
.
33
.
34
.
35
}Hydraulic_Parameter_T;
36
37
.
38
.
39
.
40
.
41
42
43
// hydraulic class
44
typedef struct
45
{
46
Hydraulic_State_E e_State;
47
Hydraulic_Parameter_T t_Paramter;
48
49
Hydraulic_Filter_State_E e_FilterTank;
50
Hydraulic_Filter_State_E e_FilterFeedLine;
51
52
Hydraulic_Temperature_Sensor_T t_TempSensorTank;
53
Hydraulic_Temperature_Sensor_T t_TempSensorFeedLine;
54
.
55
.
56
.
57
.
58
59
}Hydraulic_T;
60
61
62
Hydraulic_State_E e_CreateHydraulic( Hydraulic *pt_Hydraulic );


So ist mal ganz grob die Header aufgebaut. Dazu kommen noch die Get und 
Set Funktionen und Funktionen, die nur von der Hydraulik benötigt 
werden. Diese stehen im c File als static.

Alle hydraulischen Geräte wie Zylinder, Pumpen, Motoren usw haben wieder 
ein extra Modul. Die Temperatursensoren und Filter sind im Hydraulik, da 
dort Eigenschaften der Hydraulik wie Drücke, Temperaturen und 
Filterstati erfasst werden. Anhand dieser Informationen kann dann wieder 
ein Motor entscheiden, ob er überhaupt drehen kann/darf ( weil zum 
Beispiel kein Speisedruck ansteht ).
Hoffe, dass ich den Aufbau etwas besser beschreiben konnte

von Markus L. (rollerblade)


Lesenswert?

Stefan schrieb:
> um möglicht wenig impact im code zu haben :
> - mach alle member der struct private
> - implementiere setter und getter für jeden member
Why getter and setter methods are evil: 
http://www.javaworld.com/article/2073723/core-java/why-getter-and-setter-methods-are-evil.html

von A. S. (Gast)


Lesenswert?

Hallo Nils,

die eigentliche Architektur wird noch nicht klarer, aber jetzt kann ich 
zumindest eine Frage formulieren, am Beispiel eine Temperatur-Sensors:

Nils schrieb:
1
typedef struct
2
{
3
 uint16_t u16_Channel;
4
 uint8_t u8_Temperature;
5
 ...
6
} Hydraulic_Temperature_Sensor_T;
7
 
8
 // hydraulic class
9
 typedef struct
10
 {
11
  ...
12
  Hydraulic_Temperature_Sensor_T t_TempSensorTank;
13
  Hydraulic_Temperature_Sensor_T t_TempSensorFeedLine;
14
  ....

Ich verstehe folgendes nicht:
a) Warum ist der Temperatur-Sensor-Typ im Hydraulik-Modul definiert. Ich 
hätte jetzt gedacht, die werden in anderen Modulen ähnlich verwendet und 
es gibt übergeordnete Zugriffsfunktionen (Treiber für 
Temperatursensoren)

b) Wie wird t_TempSensorTank gefüllt? Kannst Du da ein paar Beispiele 
geben, wie und wann Channel gesetzt wird und wann die Temperatur?

c) was von t_TempSensorTank wird außerhalb von Hydraulik.c verwendet? 
Ist die Struktur komplett öffentlich oder privat für Hydraulik.c?

> Dazu kommen noch die Get und Set Funktionen und Funktionen, die nur
> von der Hydraulik benötigt werden. Diese stehen im c File als static.

Nenn mal da ein Beispiel.

von Strubi (Gast)


Lesenswert?

Moin,

ich schmeiss mal noch einen rein, erstaunlich, dass es noch keiner getan 
hat: Python-Wrappen. Dabei zeigt sich meist, ob der Code im Sinne einer 
Bibliothek brauchbar und robust ist.
Das Wrappen muss man nicht zwingend von Hand machen, man kann auch den 
Boost-Template-Wust bemühen. Die Unit-Tests sind dann schliesslich 
Python-Scripte, und wenn man Eignerschaft der Strukturen sauber 
definiert hat, kommt typischerweise ein robustes Objekt-Kernel bei rum, 
und die Applikation beherrscht gleich Skripting. Man muss sich nur 
dementsprechend auch mit Python-Paradigmen beschäftigen 
(Referenz-Counting, Eignerschaft/Besitzabhängigkeiten von Objekten)
Nur was mir oben ins Auge sticht: Was ist mit der Fehlerbehandlung? Kann 
der Getter nie fehlschlagen? Je nachdem wie weit eine 
Validierung/Verifizierung gehen muss, sollte ein Fehlerfall eigentlich 
immer abgedeckt werden, insbesondere bei Scripting.

von Mark B. (markbrandis)


Lesenswert?

Nils schrieb:
> Wie beispielsweise in C++ hat jede Funktion Zugriff auf den this Zeiger

Mit Sicherheit nicht.

Was Du wohl meintest ist, dass eine Instanz einer Klasse (also ein 
Objekt) Zugriff auf den this-Zeiger hat.

Ach und ja:

1
void v_GetTemperatureHydraulicOil( Hydraulic *pt_Hydraulic )

Sowas ist unschön. Man erwartet vom Namen her eine Getter-Funktion, aber 
da passt dann der Rückgabewert void eben nicht dazu.

: Bearbeitet durch User
von R. R. (elec-lisper)


Lesenswert?

Was man natürlich noch beachten sollte: Architektonische
Meisterwerke sind bei kleinen und überschaubaren Projekten
nicht wirklich notwendig, und muss auch nicht unbedingt der
Wartbarkeit des Codes beitragen.
Man muss immer vor Augen behalten in welche Richtungen sich
ein Programm entwickeln wird. Wo kommen neue Features hinzu?
Und an den Stellen refaktoriert man dann gezielt auf Erweiterbarkeit
und Verkapselung.

Wenn das Programm gegen Geld programmiert wird muss man auch immer
die Kosten/Aufwände im Auge behalten, und ob es sich jetzt wirklich
lohnt soviel zu refaktorieren - zumal die Änderungen ja auch
wieder getestet werden müssen (ohne automatisierte Tests ist das
dann immer manueller Aufwand).

von Nils (Gast)


Lesenswert?

Achim S. schrieb:
> Ich verstehe folgendes nicht:
> a) Warum ist der Temperatur-Sensor-Typ im Hydraulik-Modul definiert. Ich
> hätte jetzt gedacht, die werden in anderen Modulen ähnlich verwendet und
> es gibt übergeordnete Zugriffsfunktionen (Treiber für
> Temperatursensoren)
>
> b) Wie wird t_TempSensorTank gefüllt? Kannst Du da ein paar Beispiele
> geben, wie und wann Channel gesetzt wird und wann die Temperatur?
>
> c) was von t_TempSensorTank wird außerhalb von Hydraulik.c verwendet?
> Ist die Struktur komplett öffentlich oder privat für Hydraulik.c?

zu a) Vom Temperatursensor gibt es nur 2 in der Anlage, daher ist die 
Definition in der Hydraulik. Klar könnte man das wieder in ein extra 
Modul packen. In der Struktur ist ebenfalls noch ein analoger Eingang 
enthalten bzw die Schnittstelle dazu.

zu b) Im Init vom Hydraulik Modul wird der oben beschriebene analoge 
Eingang initialisiert, dafür braucht man den Channel ( ist im Endeffekt 
der Pin an der Steuerung).  Die Temperatur wird anhand des Rohwertes vom 
analogen Eingang berechnet und in u8_Temperature gespeichert.

zu c) In einigen anderen Modulen wird halt die Temperatur benötigt. 
Daher gibts eine Get Funktion

Beispiel für eine Funktion die "privat" ist:
1
 static void v_RunTask20ms( Hydraulic_T *pt_Hydraulic )

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.