www.mikrocontroller.net

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


Autor: Alexander I. (daedalus)
Datum:

Bewertung
0 lesenswert
nicht 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:
void Trigger()
{
 FSMSwitch();   // State = NextState (wird als Monitorfunktion gestartet)
 FSMDispatch(); // Zielfunktion aufrufen, je nach State
 FSMPolling();  // Nachsehen ob eine Nachricht für die FSM vorliegt und ggf
                // NextState setzen.
}

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

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Olaf Stieleke (olaf2001)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Alexander I. (daedalus)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hier mal ein Beispiel fürs Display:
class Disp : public Basis
{
// 
// ENUMERATIONS:
// =============
  
  enum Disp_States {IDLE,
                    INIT,
                    POWERUP,
                    POWERDOWN};
// 
// ATTRIBUTES:
// ===========

private:
  Disp_States State, NextState;
  
//
// PROTOTYPES OF DISP:
// =====================
  
private:
  __monitor void FSMSwitch(void);
  unsigned char FSMDispatch(void);
  void FSMPolling(void);
  unsigned char PowerDown(void);
  unsigned char PowerUp(void);

public:
  Disp();
  unsigned char Trigger (void);

Disp::Disp()
// Konstruktor
{
  State=IDLE;
  NextState=INIT;
}

unsigned char Disp::Trigger (void)
{
  unsigned char ReturnVal;
  FSMSwitch();
  ReturnVal = FSMDispatch();
  FSMPolling();
  return ReturnVal;
}

__monitor void Disp::FSMSwitch(void)
{
  State = NextState;
}

unsigned char Disp::FSMDispatch(void)
{
    switch (State)
      {
      case INIT:
        FuncVal = Init();
        break;
      case POWERUP:
        FuncVal = PowerUp();
        break;
      case POWERDOWN:
        FuncVal = PowerDown();
        break;
      default:
        break;
      }
  }
  return FuncVal;
}

void Disp::FSMPolling(void)
// This function polls for new events and triggers the appropriate NextState
{
  if(TargetValue[mDISP]==T_DISP_POWERUP)
  {
    NextState=POWERUP;
    TargetValue[mDISP] = T_DISP_IDLE;
  }
  if(TargetValue[mDISP]==T_DISP_POWERDOWN)
  {
    NextState=POWERDOWN;
    TargetValue[mDISP] = T_DISP_IDLE;
  }
}


void main()
{
 Disp *MyDisp = new Disp();
 RS232 *MyRS232 = new RS232();
 while(true)
 {
  MyDisp->Trigger();
  MyRS232->Trigger();
 }

}

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.

Autor: Alexander I. (daedalus)
Datum:

Bewertung
0 lesenswert
nicht 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 :)

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
OK. Ich hab mal einen Vorschlag zur Ansicht, wie sowas
aussehen könnte.
#include <iostream>
#include <vector>

using namespace std;

// *********************************************************
// Funktor, Basisklasse
//
// Ein Funktor ist ein Objekt, welches einen Funktionsaufruf kapselt
//
class TFunctor
{
  public:
    virtual unsigned char operator()() = 0;
};

// Funktor, konkrete Klasse die auf die aufzurufende Klasse geht
//
// Diese Klasse verbindet den Funktionspointer mit dem konkreten Objekt
// welches aufgerufen werden soll
template <class T> class FSMFunctor : public TFunctor
{
  private:
    unsigned char (T::*fpt)();     // pointer to member function
    T* pObject;                    // pointer to object

  public:
    FSMFunctor(T* pObject_, unsigned char(T::*fpt_)())
    : pObject( pObject_ ),
      fpt( fpt_ )
    {
    }

    virtual unsigned char operator()()
    {
      return (*pObject.*fpt)();
    }
};

// *********************************************************
// FSMDispatchFunction verbindet einen Status mit einer aufzurufenden
// Funktion (über einen Funktor)
//
class FSMDispatchFunction
{
  public:
    FSMDispatchFunction( int State_, TFunctor* pFunc_ )
    : State( State_ ),
      pFunction( pFunc_ )
    {
    }
  
    virtual unsigned char operator()()
    {
      return (*pFunction)();
    }

    int GetState() const { return State; }

  protected:
    int State;                // der Status des Objektes
    TFunctor* pFunction;      // Funktion die fuer diesen Status gerufen werden soll
};     

// *********************************************************
// Basisklasse für eine Statemachine
//
class FSM
{
  public:
    virtual ~FSM() = 0 { };
    
    unsigned char Trigger();
    void RegisterDispatch( int State, TFunctor* Function );
    
  protected:
    virtual void FSMSwitch();
    virtual void FSMPolling();
    virtual unsigned char FSMDispatch();
    
    unsigned char State;
    unsigned char NextState;
    
    std::vector<FSMDispatchFunction*> Functions;
};

void FSM::RegisterDispatch( int State, TFunctor* Function )
{
  Functions.push_back( new FSMDispatchFunction( State, Function ) );
}

void FSM::FSMSwitch()
{
  State = NextState;
}

void FSM::FSMPolling()
{
}

unsigned char FSM::FSMDispatch()
{
  for( size_t i = 0; i < Functions.size(); ++i ) {
    if( Functions[i]->GetState() == State ) {
      return (*Functions[i])();
    }
  }

  return 0;
}

unsigned char FSM::Trigger()
{
  unsigned char ReturnVal;

  FSMSwitch();
  ReturnVal = FSMDispatch();
  FSMPolling();

  return ReturnVal;
}

// *********************************************************

class Disp : public FSM
{
  public:
    Disp();

  private:
    unsigned char Init();
    unsigned char PowerDown();
    unsigned char PowerUp();

    static const int IdleState = 0;
    static const int InitState = 1;
    static const int PowerUpState = 2;
    static const int PowerDownState = 3;
};

Disp::Disp()
{
  State = IdleState;
  NextState = InitState;
  
  RegisterDispatch( InitState, new FSMFunctor<Disp>( this, &Disp::Init ) );
  RegisterDispatch( PowerUpState, new FSMFunctor<Disp>( this, &Disp::PowerUp ) );
  RegisterDispatch( PowerDownState, new FSMFunctor<Disp>( this, &Disp::PowerDown ) );
}

unsigned char Disp::Init()
{
  cout << "Disp: Init" << std::endl;
  
  NextState = PowerUpState;
  
  return 0;
}

unsigned char Disp::PowerUp()
{
  cout << "Disp: Power up" << std::endl;
  
  NextState = PowerDownState;
  
  return 0;
}

unsigned char Disp::PowerDown()
{
  cout << "Disp: Power down" << std::endl;
  
  NextState = InitState;
  
  return 0;
}

// ******************************************
class RS232 : public FSM
{
  public:
    RS232();
    
  private:
    unsigned char Init();
    unsigned char Dial();
    unsigned char HangUp();

    static const int IdleState = 0;
    static const int InitState = 1;
    static const int DialState = 2;
    static const int HangUpState = 3;
};

RS232::RS232()
{
  State = IdleState;
  NextState = InitState;
  
  RegisterDispatch( InitState, new FSMFunctor<RS232>( this, &RS232::Init ) );
  RegisterDispatch( DialState, new FSMFunctor<RS232>( this, &RS232::Dial ) );
  RegisterDispatch( HangUpState, new FSMFunctor<RS232>( this, &RS232::HangUp ) );
}  

unsigned char RS232::Init()
{
  cout << "RS232: Init\n";
  NextState = RS232::DialState;
  
  return 1;
}

unsigned char RS232::Dial()
{
  cout << "RS232: Dial\n";
  NextState = RS232::HangUpState;

  return 1;
}

unsigned char RS232::HangUp()
{
  cout << "RS232: Hanging up\n";
  NextState = RS232::InitState;

  return 1;
}

// ******************************************
int main()
{
  Disp* pMyDisp = new Disp();
  RS232* pMyRS232 = new RS232();

  while(true) {
    pMyDisp->Trigger();
    pMyRS232->Trigger();
  }
}

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).

Autor: Alexander I. (daedalus)
Datum:

Bewertung
0 lesenswert
nicht 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?

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.