Forum: Mikrocontroller und Digitale Elektronik OOP: Grundlegende Frage zur Vererbung


von Alexander I. (daedalus)


Lesenswert?

Hallo,

ich habe ein bereits lauffähiges System auf einem µC, möchte es aber 
noch im objektorientierten Sinne optimieren. Programmiert wird mit C++:


Ich habe 4 Klassen:

Basis (Standardfunktionen die alle haben)
FSM1 : Basis (z.B. LCD-Display-Treiber)
FSM2 : Basis (z.B. RS232-Treiber)
FSM3 : Basis (z.B. Transceiver)

FSM steht dabei für Finite State Machine. Jede FSM hat dabei 3 Funktion, 
die bei jedem main-Schleifendurchlauf für die jeweiligen Objekte 
nacheinander augerufen werden:
1
void Trigger()
2
{
3
 FSMSwitch();   // State = NextState (wird als Monitorfunktion gestartet)
4
 FSMDispatch(); // Zielfunktion aufrufen, je nach State
5
 FSMPolling();  // Nachsehen ob eine Nachricht für die FSM vorliegt und ggf
6
                // NextState setzen.
7
}

Der Funktionsrumpf ist im Prinzip in jeder Klasse derselbe, ABER es gibt 
in jeder Klasse andere States die momentan als Enum innerhalb der 
jeweiligen Klasse realisiert sind. Also z.B. das LCD hat die States 
SHOWTEXT,CHANGEBRIGHTNESS,usw. während der RS232 z.B. SENDBYTE, 
RECEIVEBYTE als States hat. Neben den States sind natürlich auch 
Funktionsziele in jeder Klasse anders, auch die Ereignisse auf die 
gepollt wird sind unterschiedlich. Die Klassen bestehen im Wesentlichen 
aus switch-case-Anweisungen. Die Zielfunktionen der Klassen 
unterscheiden sich natürlich erheblich.

Wie schaffe ich es also, die drei Funktionen in die Klasse Basis zu 
integrieren und nur die jeweiligen Unterschiede der einzelnen Klassen 
aufzuführen? Oder ist es schlichtweg sinnvoller in jeder Klasse diese 
drei minimal unterschiedlichen Funktionen separat zu halten? Das 
Problem, was ich habe ist, dass ja "Basis" zum Initialisierungszeitpunkt 
noch nicht die States von "Display" kennt?

Ich hoffe, ihr habt mir ein paar gute Tipps :)
Danke schonmal

von Karl H. (kbuchegg)


Lesenswert?

Es ist, zumindest für mich, schwierig aus deiner Beschreibung
abzulesen, was du da eigentlich implementiert hast.
Wenn möglich, specke 2 deiner State Maschines ab und poste die mal.

> Die Klassen bestehen im Wesentlichen
> aus switch-case-Anweisungen.

Der Switch wird über den aktuellen State geführt?

Dann würde ich mal folgendes Versuchen:
Eine Datenstruktur aufbauen, die einen State mit einem
Funktionspointer verknüpft.
In die Basisklasse kommt dann eine Tabelle (ein std::vector)
in der Objekte dieser Verknüpfungen gespeichert sind.
Anstelle der switch-case Leiste, kann dann Code in
der Basisklasse diese Tabelle durchsuchen und wenn der
aktuelle State mit dem in der Tabelle angegebenen State
übereinstimmt, wird die zugehörige Funktion über den Funktions-
pointer aufgerufen.

Aber wie gesagt: Vielleicht habe ich dich auch komplett
missverstanden, was du eigentlich hast und wo du den Hebel
ansetzten möchtest.

von Olaf S. (olaf2001)


Lesenswert?

Eigentlich mußt du dir nur Gedanken darum machen, was diese drei Klassen 
da gemeinsam haben.

Da fallen mir zunächst die drei Methoden da auf. Vielleicht sind da auch 
noch ein paar Membervariablen, die in ALLEN drei Klassen auftauchen und 
ein paar Methoden, die sich nur unwesentlich unterscheiden.

Mit diesem Wissen baust du nun deine Basis-Klasse auf - mit den 
Membervariablen, die die drei Klassen gemein haben, mit den paar 
Methoden und vor allem mit den drei "Arbeiter"-Methoden. Diese drei 
Methoden werden als virtuell und abstrakt deklariert - damit ist auch 
klar, das Basis niemals direkt instantiiert wird.

Ist das passiert, brauchst du nur noch von deiner Basis ableiten und die 
drei abstrakten Arbeiter-Methoden implementieren.

von Karl H. (kbuchegg)


Lesenswert?

Olaf Stieleke wrote:
> Eigentlich mußt du dir nur Gedanken darum machen, was diese drei Klassen
> da gemeinsam haben.
>
> Da fallen mir zunächst die drei Methoden da auf. Vielleicht sind da auch
> noch ein paar Membervariablen, die in ALLEN drei Klassen auftauchen und
> ein paar Methoden, die sich nur unwesentlich unterscheiden.
>
> Mit diesem Wissen baust du nun deine Basis-Klasse auf - mit den
> Membervariablen, die die drei Klassen gemein haben, mit den paar
> Methoden und vor allem mit den drei "Arbeiter"-Methoden. Diese drei
> Methoden werden als virtuell und abstrakt deklariert - damit ist auch
> klar, das Basis niemals direkt instantiiert wird.
>
> Ist das passiert, brauchst du nur noch von deiner Basis ableiten und die
> drei abstrakten Arbeiter-Methoden implementieren.

So wie ich das verstanden habe, hat er genau das ja schon gemacht.
Ihm geht es um die Arbeiter Methode, die im wesentlichen nichts
anderes macht als je nach momentanem State zu einer Bearbeitungs-
funktion zu verzweigen. Und das ist ja im Prinzip in allen 3 Fällen
identisch: Ein State ist logisch mit einer Funktion verknüpft und
je nach momenatnem aktuellen State wird die zugehörige Funktion
aufgerufen.
Das Pattern ist also in allen Fällen gleich, lediglich die Namen
der States sowie die Namen der Bearbeitungsfunktionen ändern sich.

Wenn ich ihn jetzt richtig interpretiert habe, dann möchte er genau
diesen Verteilmechanismus in die Basisklasse hinunterziehen, was
ja durchaus sinnvoll ist.
Die abgeleiteten Klassen definieren dann nur noch die States und
die zum jeweiligen State gehörende Bearbeitungsfunktion, registrieren
genau dieses Tuppel bei der Basisklasse und die Dispatch Funktion
in der Basisklasse kümmert sich dann darum, dass die jeweils richtige
Funktion, passend zum aktuellen State, aufgerufen wird.

von Alexander I. (daedalus)


Lesenswert?

Hier mal ein Beispiel fürs Display:
1
class Disp : public Basis
2
{
3
// 
4
// ENUMERATIONS:
5
// =============
6
  
7
  enum Disp_States {IDLE,
8
                    INIT,
9
                    POWERUP,
10
                    POWERDOWN};
11
// 
12
// ATTRIBUTES:
13
// ===========
14
15
private:
16
  Disp_States State, NextState;
17
  
18
//
19
// PROTOTYPES OF DISP:
20
// =====================
21
  
22
private:
23
  __monitor void FSMSwitch(void);
24
  unsigned char FSMDispatch(void);
25
  void FSMPolling(void);
26
  unsigned char PowerDown(void);
27
  unsigned char PowerUp(void);
28
29
public:
30
  Disp();
31
  unsigned char Trigger (void);
32
33
Disp::Disp()
34
// Konstruktor
35
{
36
  State=IDLE;
37
  NextState=INIT;
38
}
39
40
unsigned char Disp::Trigger (void)
41
{
42
  unsigned char ReturnVal;
43
  FSMSwitch();
44
  ReturnVal = FSMDispatch();
45
  FSMPolling();
46
  return ReturnVal;
47
}
48
49
__monitor void Disp::FSMSwitch(void)
50
{
51
  State = NextState;
52
}
53
54
unsigned char Disp::FSMDispatch(void)
55
{
56
    switch (State)
57
      {
58
      case INIT:
59
        FuncVal = Init();
60
        break;
61
      case POWERUP:
62
        FuncVal = PowerUp();
63
        break;
64
      case POWERDOWN:
65
        FuncVal = PowerDown();
66
        break;
67
      default:
68
        break;
69
      }
70
  }
71
  return FuncVal;
72
}
73
74
void Disp::FSMPolling(void)
75
// This function polls for new events and triggers the appropriate NextState
76
{
77
  if(TargetValue[mDISP]==T_DISP_POWERUP)
78
  {
79
    NextState=POWERUP;
80
    TargetValue[mDISP] = T_DISP_IDLE;
81
  }
82
  if(TargetValue[mDISP]==T_DISP_POWERDOWN)
83
  {
84
    NextState=POWERDOWN;
85
    TargetValue[mDISP] = T_DISP_IDLE;
86
  }
87
}
88
89
90
void main()
91
{
92
 Disp *MyDisp = new Disp();
93
 RS232 *MyRS232 = new RS232();
94
 while(true)
95
 {
96
  MyDisp->Trigger();
97
  MyRS232->Trigger();
98
 }
99
100
}

TargetValue[] ist ein globaler Array wo für die jeweiligen Objekttypen 
Kommandos abgespeichert werden können. Alle groß geschriebenen Werte 
sind defines mit primitiven Datentypen oder enums.

von Alexander I. (daedalus)


Lesenswert?

Hallo Karl-Heinz,

du hast den Nagel absolut auf den Kopf getroffen. Mit dem Sourcecode 
zusammen ist es hoffentlich klar worauf ich hinaus will :)

von Karl H. (kbuchegg)


Lesenswert?

Alex Wurst wrote:
> Hier mal ein Beispiel fürs Display:

Es geht im wesentlichen um die Funktion
FSMDispatch, die du in die Basisklasse
verfrachten möchtest.

Hmm. Der enum wird Probleme machen, da wird wohl eine
Ausweiche auf einen int und zusätzlich ein paar Konstante
fällig werden. Das andere sind Pointer auf Memberfunktionen.
Die sind in C++ richtig ekelig (weil der Klassenname teil
der Pointer Signatur ist). Das wird wohl auf etwas casten
raus laufen. Ev. könnte man mit einer Zwischenklasse da was
reissen (getreu dem OOP Motto: Wenn was nicht funktioniert,
erfinde erst mal eine Klasse dafür).

Gib mir etwas Zeit. Die grundsätzliche Idee hab ich schon im
Kopf. Da sind mir aber noch zuviele Details die schief gehen
können und die ich abklären möchte.

von Karl H. (kbuchegg)


Lesenswert?

OK. Ich hab mal einen Vorschlag zur Ansicht, wie sowas
aussehen könnte.
1
#include <iostream>
2
#include <vector>
3
4
using namespace std;
5
6
// *********************************************************
7
// Funktor, Basisklasse
8
//
9
// Ein Funktor ist ein Objekt, welches einen Funktionsaufruf kapselt
10
//
11
class TFunctor
12
{
13
  public:
14
    virtual unsigned char operator()() = 0;
15
};
16
17
// Funktor, konkrete Klasse die auf die aufzurufende Klasse geht
18
//
19
// Diese Klasse verbindet den Funktionspointer mit dem konkreten Objekt
20
// welches aufgerufen werden soll
21
template <class T> class FSMFunctor : public TFunctor
22
{
23
  private:
24
    unsigned char (T::*fpt)();     // pointer to member function
25
    T* pObject;                    // pointer to object
26
27
  public:
28
    FSMFunctor(T* pObject_, unsigned char(T::*fpt_)())
29
    : pObject( pObject_ ),
30
      fpt( fpt_ )
31
    {
32
    }
33
34
    virtual unsigned char operator()()
35
    {
36
      return (*pObject.*fpt)();
37
    }
38
};
39
40
// *********************************************************
41
// FSMDispatchFunction verbindet einen Status mit einer aufzurufenden
42
// Funktion (über einen Funktor)
43
//
44
class FSMDispatchFunction
45
{
46
  public:
47
    FSMDispatchFunction( int State_, TFunctor* pFunc_ )
48
    : State( State_ ),
49
      pFunction( pFunc_ )
50
    {
51
    }
52
  
53
    virtual unsigned char operator()()
54
    {
55
      return (*pFunction)();
56
    }
57
58
    int GetState() const { return State; }
59
60
  protected:
61
    int State;                // der Status des Objektes
62
    TFunctor* pFunction;      // Funktion die fuer diesen Status gerufen werden soll
63
};     
64
65
// *********************************************************
66
// Basisklasse für eine Statemachine
67
//
68
class FSM
69
{
70
  public:
71
    virtual ~FSM() = 0 { };
72
    
73
    unsigned char Trigger();
74
    void RegisterDispatch( int State, TFunctor* Function );
75
    
76
  protected:
77
    virtual void FSMSwitch();
78
    virtual void FSMPolling();
79
    virtual unsigned char FSMDispatch();
80
    
81
    unsigned char State;
82
    unsigned char NextState;
83
    
84
    std::vector<FSMDispatchFunction*> Functions;
85
};
86
87
void FSM::RegisterDispatch( int State, TFunctor* Function )
88
{
89
  Functions.push_back( new FSMDispatchFunction( State, Function ) );
90
}
91
92
void FSM::FSMSwitch()
93
{
94
  State = NextState;
95
}
96
97
void FSM::FSMPolling()
98
{
99
}
100
101
unsigned char FSM::FSMDispatch()
102
{
103
  for( size_t i = 0; i < Functions.size(); ++i ) {
104
    if( Functions[i]->GetState() == State ) {
105
      return (*Functions[i])();
106
    }
107
  }
108
109
  return 0;
110
}
111
112
unsigned char FSM::Trigger()
113
{
114
  unsigned char ReturnVal;
115
116
  FSMSwitch();
117
  ReturnVal = FSMDispatch();
118
  FSMPolling();
119
120
  return ReturnVal;
121
}
122
123
// *********************************************************
124
125
class Disp : public FSM
126
{
127
  public:
128
    Disp();
129
130
  private:
131
    unsigned char Init();
132
    unsigned char PowerDown();
133
    unsigned char PowerUp();
134
135
    static const int IdleState = 0;
136
    static const int InitState = 1;
137
    static const int PowerUpState = 2;
138
    static const int PowerDownState = 3;
139
};
140
141
Disp::Disp()
142
{
143
  State = IdleState;
144
  NextState = InitState;
145
  
146
  RegisterDispatch( InitState, new FSMFunctor<Disp>( this, &Disp::Init ) );
147
  RegisterDispatch( PowerUpState, new FSMFunctor<Disp>( this, &Disp::PowerUp ) );
148
  RegisterDispatch( PowerDownState, new FSMFunctor<Disp>( this, &Disp::PowerDown ) );
149
}
150
151
unsigned char Disp::Init()
152
{
153
  cout << "Disp: Init" << std::endl;
154
  
155
  NextState = PowerUpState;
156
  
157
  return 0;
158
}
159
160
unsigned char Disp::PowerUp()
161
{
162
  cout << "Disp: Power up" << std::endl;
163
  
164
  NextState = PowerDownState;
165
  
166
  return 0;
167
}
168
169
unsigned char Disp::PowerDown()
170
{
171
  cout << "Disp: Power down" << std::endl;
172
  
173
  NextState = InitState;
174
  
175
  return 0;
176
}
177
178
// ******************************************
179
class RS232 : public FSM
180
{
181
  public:
182
    RS232();
183
    
184
  private:
185
    unsigned char Init();
186
    unsigned char Dial();
187
    unsigned char HangUp();
188
189
    static const int IdleState = 0;
190
    static const int InitState = 1;
191
    static const int DialState = 2;
192
    static const int HangUpState = 3;
193
};
194
195
RS232::RS232()
196
{
197
  State = IdleState;
198
  NextState = InitState;
199
  
200
  RegisterDispatch( InitState, new FSMFunctor<RS232>( this, &RS232::Init ) );
201
  RegisterDispatch( DialState, new FSMFunctor<RS232>( this, &RS232::Dial ) );
202
  RegisterDispatch( HangUpState, new FSMFunctor<RS232>( this, &RS232::HangUp ) );
203
}  
204
205
unsigned char RS232::Init()
206
{
207
  cout << "RS232: Init\n";
208
  NextState = RS232::DialState;
209
  
210
  return 1;
211
}
212
213
unsigned char RS232::Dial()
214
{
215
  cout << "RS232: Dial\n";
216
  NextState = RS232::HangUpState;
217
218
  return 1;
219
}
220
221
unsigned char RS232::HangUp()
222
{
223
  cout << "RS232: Hanging up\n";
224
  NextState = RS232::InitState;
225
226
  return 1;
227
}
228
229
// ******************************************
230
int main()
231
{
232
  Disp* pMyDisp = new Disp();
233
  RS232* pMyRS232 = new RS232();
234
235
  while(true) {
236
    pMyDisp->Trigger();
237
    pMyRS232->Trigger();
238
  }
239
}

Die new stören mich noch und den std::vector müsste man wahrscheinlich
mit einem statischen Array ersetzen (damits auf einem µC noch geht).

von Alexander I. (daedalus)


Lesenswert?

Hallo,

die Lösung sieht mir ja doch sehr abstrakt aus, aber ich glaube das 
könnte so funktionieren :). Die große Frage, die ich mir dabei stelle: 
Was macht der Compiler draus? Gerade das Template ist ja sehr 
"gefährlich" im Zusammenhang mit Codegröße. Die 2. Frage ist, wie du 
schon angesprochen hast die new's, denn die knabbern ja alle 
unaufhörlich am Heap.
Zusätzlich bräuchte man bei statischen Arrays ja noch eine Art 
Verwaltung, in Form eines Stacks o.Ä.
Aber eine Frage hab ich noch: Wieso hast du überall static const int's 
verwendet anstatt eines enums? Den könnte man doch ebenfalls auf einen 
int casten?

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.