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:
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:
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
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.
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)
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.
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:>
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.
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 :)
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.
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.
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
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.
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
structthing;
12
typedefstructthingTHING;
13
14
// Deklaration von Funktionen die auf THING arbeiten können
15
THING*thing_new(intarg);
16
voidthing_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
structthing{
10
intsize;
11
charbuf[256];
12
intlast_op;
13
};
14
15
//
16
// Öffentliche Funktionen
17
//
18
THING*thing_new(intarg){
19
...
20
}
21
22
voidthing_write(THING*t,char*message){
23
...
24
}
Man beachte die verwendete Namenskonvention. Funktionen die mit thing_
beginnen arbeiten auf THING*. Statt
Ü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.
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
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.
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.
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.
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.
>> 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:>
>> 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.
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
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.
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
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.
> 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 ....
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
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
Hallo Nils,
die eigentliche Architektur wird noch nicht klarer, aber jetzt kann ich
zumindest eine Frage formulieren, am Beispiel eine Temperatur-Sensors:
Nils 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?
> 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.
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.
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:
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).
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: