OOP für Anfänger - Einstieg mit nachvollziehbarer Erklärung

Hallo,

vielleicht muss man den Zwischenschritt gehen den ich gerade mache. ? Derzeit versuche ich alles strukturiert zu programmieren. Ich nutze also struct (class) um zusammengehörende Variablen (Member) auch zusammengefasst zu haben. Der Rest sind noch normale Funktionen die damit arbeiten. Wenn ich OOP richtig verstanden habe, wäre dann der nächste Schritt zu OOP die Funktionen in die Struktur/Klasse "zu werfen" (dann Methoden genannt) um auch diese zu kapseln. Meine Strukturen nutze ich derzeit mit vollen globalen (public) Zugriff. Alle nicht konstanten Member können von außen verändert werden. Derzeit kämpfe ich mit der sinnvollen Aufteilung der Strukturen. Sonst mutiert das wie combie sagt immer Gottklassen, also eine wo alles was geht reingestopft wurde.

Viele Bsp. zu OOP beginnen mit dem Thema Auto. Der Klassenname heißt Auto. Welche Eigenschaften der späteren Objekte möchte ich verarbeiten? Marke, Modell, Baujahr, Leistung, Leergewicht usw. Das wären dann die Member in der Klasse. Danach richte ich mich derzeit. Wenn dann noch die nötigen Methoden eingebaut sind die diese Daten verarbeiten hat man die Kapselung die OOP ausmacht. Was public und was privat sein soll muss man auch noch festlegen. Interner oder externer Zugriff für Member und Methoden. Soweit mein Theoriestand.

Vielleicht hilft das zum reindenken.

Ich halte die struct-Variante als OOP für nicht gut fürs Verständnis der OOP.
Das kann damit zusammen hängen, das aus ANSI-C structs für mich Datenstrukturen repräsentieren.

Wenn man für OOP konsequent das Schlüsselwort class benutzt, sieht man gleich auf Anhieb den Unterschied.

Gruß Tommy

Doc_Arduino:
Wenn dann noch die nötigen Methoden eingebaut sind die diese Daten verarbeiten hat man die Kapselung die OOP ausmacht. Was public und was privat sein soll muss man auch noch festlegen.

Das ist die falsche Reihenfolge. Kapselung bedeutet gerade die Daten vor äußerem Zugriff zu schützen. Erst wenn du diese private machst (was bei class im Gegensatz zu struct normal der Fall ist) und nur durch Methoden zugänglich machst hast du Kapselung.

combie:
Der Vergleich hinkt also recht stark.

Ich mußte das einfach mal probieren, denn die Tatsachen widersprechen meinem Gefühl, weil CombiePin zeigt, was die IDE verbirgt, naja, was ich mir nicht ansehe.

Lassen wir ihn hinken, ich habe ihn gebraucht.

combie:
Die Frage ist da nur:
Wie bekommt man es hin, die gewohnten Denkpfade zu verlassen?

Mein Antrieb heißt Faulheit. Ich kann viel Energie aufwenden, um es mir gemütlich zu machen. Deine for-Schleife, die nichts mit OOP zu tun hat, war so schön einfach. Meine Faulheit zeigte sich interessiert. Darüber stellte ich fest, in einer Funktion mit einer Indexvariablen jonglieren zu müssen, während eine Methode ohne Index auskommt. Eine Mühe und Fehlerquelle weniger, das motiviert.

Das Beispiel mit dem Holzklotz ist Klasse, das verstehe ich sofort!

Bei "Modell" denke ich an das Bohrsche Atommodell, welches nicht die Realität ist, mit dem man aber in Teilaspekten wunderbar arbeiten kann, besser als mit den Strings in elf Dimensionen.

Das Nassi-Shneiderman-Diagramm funktioniert mit Programmiersprachen, die das mit Sprachelementen unterstützen, ebenso wie mit Assembler. Dieses Denken habe ich gelernt, weshalb mich mehrere return in einer Funktion stören. Mal sehen, ob ich "Orientierung finde".

Tommy, Serenifly - auch wieder wahr. :wink:

Bis runter in Atomstrukturen denken möchte ich jetzt nicht unbedingt. :slight_smile:

Serenifly:
Das ist die falsche Reihenfolge. Kapselung bedeutet gerade die Daten vor äußerem Zugriff zu schützen. Erst wenn du diese private machst (was bei class im Gegensatz zu struct normal der Fall ist) und nur durch Methoden zugänglich machst hast du Kapselung.

Wobei ich mir angewöhnt habe bei class zurerst die public-Methoden und danach die private Variablen anzugeben. Wenn notwendig dazwischen die protected. Das ist aber Gewohnheitssache.

Gruß Tommy

Man muss für sich selbst einen gangbaren, sauberen Weg finden und den dann konsequent einhalten. Also nicht jedes Mal ohne besonderen Grund die Schreibweise umkrempeln.

Dann sieht man auf Anhieb, was wie sichtbar ist.

Gruß Tommy

Viele Bsp. zu OOP beginnen mit dem Thema Auto. Der Klassenname heißt Auto. Welche Eigenschaften der späteren Objekte möchte ich verarbeiten? Marke, Modell, Baujahr, Leistung, Leergewicht usw. Das wären dann die Member in der Klasse. Danach richte ich mich derzeit. Wenn dann noch die nötigen Methoden eingebaut sind die diese Daten verarbeiten hat man die Kapselung die OOP ausmacht. Was public und was privat sein soll muss man auch noch festlegen. Interner oder externer Zugriff für Member und Methoden. Soweit mein Theoriestand.

Also DIESE Kurzfassung bringt mir schon mal Licht in's Dunkel.

Habe mir ja auch kürzlich diese 1000-Seite-Schwarte zur C++ Programmierung angeschafft. Da blätter ich dann parallel zum entsprechenden Kapitel und hab dann 2 Quellen zum durchblicken.

Jungs, das wird schon 8)

denn die Tatsachen widersprechen meinem Gefühl,

Guter Ansatz!
Damit spielen ist genau der richtige Weg.
Wie die Kinder.

Immer wenn man sich nicht gerade in einer akuten Notlage befindet, lohnt es sich, solchen Widersprüchen/Auffälligkeiten nach zu gehen.

Ich nenne diesen Zünder "Sprung in der Projektion"
z.B. Hier, hier,hier und hier
Naja.. wird bestimmt noch häufiger von mir kommen.

Spannendes Thema eigentlich.... auch für das Restleben, außerhalb der µC.
Hat aber auch nur am Rande mit OOP zu tun.
Wie die vielen kleinen anderen Werkzeuge auch.

Mein Antrieb heißt Faulheit. Ich kann viel Energie aufwenden, um es mir gemütlich zu machen. Deine for-Schleife, die nichts mit OOP zu tun hat, war so schön einfach. Meine Faulheit zeigte sich interessiert. Darüber stellte ich fest, in einer Funktion mit einer Indexvariablen jonglieren zu müssen, während eine Methode ohne Index auskommt. Eine Mühe und Fehlerquelle weniger, das motiviert.

Je länger ich darüber nachdenke...
Desto mehr glaube ich, dass du recht hast.
Ja, ich denke, dass Faulheit durchaus eine meiner Motivationen ist, so manches mal tief in die Werkzeugkiste zu greifen. Vielleicht sogar die treibenste.

Angst ist kein guter Ratgeber (außer in echter Not).

Faulheit scheinbar schon...

Mantra:

Wenn du etwas drei mal auf die gleiche Art getan hast,
dann mache eine Funktion/Klasse draus.

Der Vorteil der Geschichte ist ganz klar. Einmal schreiben, ordentlich debuggen und kommentieren, danach immer nur stumpf einsetzen.

Dinge auf Wiederverwendbarkeit auszulegen ist etwas Arbeit.
Austesten, probieren, und üben. Damit spielen.
Aber danach, kann man die Innereien dieser Komponente vergessen.
Nur noch einsetzen.

Das Beispiel mit dem Holzklotz ist Klasse, das verstehe ich sofort!

Schön, dass es dir gefällt!

Im Grunde ist es diese Naivität, welche es zu gewinnen gilt. Prozedurale Programmierer konzentrieren sich gerne auf die Komplexität eines Problems. Das führt dazu, dass man beim betrachten eines OO Programms den Wald vor Bäumen nicht mehr sieht. Man sieht die komplexen(vielleicht sogar noch unbekannten) OOP Sprachmittel, aber die Naivität der Objekte, ihre klaren Schnittstellen, sind eher weniger auffällig.

Der prozedurale Programmierer hat gerne den Programmfluss vor sich.
Diesen sieht er aber im OOP Programm nicht. Da sieht das alles irgendwie verworren aus.
Denn dort stehen die Dinge in Beziehung zueinander.
Und aus den Beziehungen, der Dinge untereinander, entsteht der Programmfluss automatisch.
Wenn man einzig auf den Programmfluss aus ist, wird man sich nur unter großen Verrenkungen ein Bild davon machen können.

Die Naivität macht schon einen großen Teil der OOP Denke aus.

if(digitalRead(tasterPin) == LOW)
{
   tueMitLed(HIGH);
} else
{
   tueMitLed(LOW);
}

Oft gesehenes Beispiel.... in vielen Variationen ....
Das kommt der prozeduralen Sicht sehr entgegen.
Ist also aus prozeduraler Sicht als naiv anzusehen.

Steigerungen sind leicht möglich:

tueMitLed(!digitalRead(tasterPin));

Hier werden schon Taster und LED in Beziehung gesetzt.

In, oder mit der Lib, geht es auch so:

led = taster;

Der naive prozedurale Programmierer bekommt einen Schock.
"Wie jetzt? Warum soll die Led plötzlich ein Taster sein?"

Ich, als naiver OOPler, denke, ich brauche einen Taster.
Ich brauche dauernd Taster.
Also mache ich mir, ein für alle male, einen Taster.

Was muss dieser Taster können?
Was muss ich damit anstellen können?

Im Grunde sind das nur zwei Sachen.
Ich muss ihn einmal initialisieren, ab dann ist er bereit, seinen Job zu tun.

Und sein Job?
Immer, wenn ich ihn frage, ihn in Software anfasse, hat er mir seinen Zustand zu liefern.
Fertig!
Damit kann man 99.9% aller Tasterabfragen/Anwendungen abhandeln.

Mit der Led ist es ähnlich.
Initialisieren, damit sie bereit ist das zu tun, was man von ihr will.
Zustand ändern, wenn die Nachricht kommt.

led = taster;

Der Taster liefert seinen Zustand, was anderes kann er nicht, und dieser Zustand wird der LED zugewiesen. Diese empfängt die Nachricht und tut, was zu tun ist.

Einen Tick weiter in OOP gedacht, würde man das "Subjekt Observer Design Pattern" implementieren.
Der Taster würde zum Subjekt, die LED zum Observer.
In setup(), oder gar noch vorher, in einem Konstuktor, würde man dann die Beziehung der beiden etablieren, und im Rest des Programms nichts mehr davon sehen.

OK, in meiner Lib ging mir das zu weit.
Das stand ganz klar im Widerspruch zu "nur das nötigste".
Aber dennoch steht es jedem frei, von den Klassen zu erben, und das dann auf dem Wege nachzurüsten.

Das ist dann aber nicht mehr nur OOP, sondern auch schon Teil der "Ereignis orientierten Programmierung"
Ach...
Das mischt sich doch sowieso alles....


:o :o :o :o :o

combie:
Der prozedurale Programmierer hat gerne den Programmfluss vor sich.
Diesen sieht er aber im OOP Programm nicht. Da sieht das alles irgendwie verworren aus.
Denn dort stehen die Dinge in Beziehung zueinander.
Und aus den Beziehungen, der Dinge untereinander, entsteht der Programmfluss automatisch.
Wenn man einzig auf den Programmfluss aus ist, wird man sich nur unter großen Verrenkungen ein Bild davon machen können.

An dem Punkt stehe ich derzeit. Ich feile gern am Programmfluss. Manchmal erstelle ich mir auch Flussdiagramme. Und aktuell versuche ich über den Zwischenschritt der Strukturierung den Weg zu OOP zu finden. Ich weiß zwar was OOP ausmacht nur muss die Verinnerlichung noch vollzogen und beendet werden. Ich bin heute immer noch der Meinung, wenn man nicht vernünftig strukturieren kann, kann man auch keine vernünftigen Klassen schreiben. Dann baut man auch nur "Gott-Klassen" statt "Gott-Strukturen". Man hätte nicht viel gewonnen. Im Buch "Der C++ Programmierer" wird das ja gut beschrieben. Nur stecke ich eben leider noch in der prozeduralen Denkweise fest von der ich mich langsam löse und benötige für mich eine andere Art und Weise des Übergangs. Im Grunde ist ja für mich alles klar zu OOP.

combie:
Ich, als naiver OOPler, denke, ich brauche einen Taster.
Ich brauche dauernd Taster.
Also mache ich mir ein für alle male, einen Taster.

Ist der in der CombieLib?

Gruß Tommy

Ich feile gern am Programmfluss.

Ja, glaube ich dir...

Aber dann wird dir dieses, vermutlich, so gar nicht schmecken:
Inversion of Control
Konsequent eingesetzt, verschwindet der Programmfluss nahezu vollständig, aus dem Sichtbereich.

Wird im Arduino "Framework" an einigen Stellen eingesetzt.
Ist aber leider auch mit "Kosten" verbunden, welche sich um so stärker auswirken, je kleiner der µC.
Aber das ist bei vielen Ideen so.....


Ist der in der CombieLib?

Ja!

#include "CombiePin.h"

/*
 * Taster zwischen Pin 2 und GND, interner Pullup.
 * die LED an Pin 13 mit Vorwiderstand gegen GND
 */

Combie::Pin::TasterGND<2>  taster;
Combie::Pin::OutputPin<13> led;

void setup() 
{
  taster.initPullup();
  led.init();
}

void loop() 
{
  led = taster;
}

Hallo,

Ich hab hier auch mitgelesen, sehr Spannendes Thema.

Ich komme aus der php Welt, ich hab schon vor langer Zeit angefangen alles in Klassen schreiben. In C++ fehlt mir einfach noch das Wie um überhaupt eine vernüftige Klasse zu erstellen. Aber mit dem Arduino "Framework" kann man zumindest leicht Funktionen basteln.
Ich hab mir die wichtigsten Programmroutinen Funktionen geschrieben, Taster abfragen, Toggeln, Timing, etc. Die Abfrage eines Sensors kommt eigentlich auch immer in eine Funktion. So hab ich getestete Codeteile um die ich mir keine Gedanken machen muss, sondern "einfach" alles in Beziehung zueinander setzen. Muss das Programm an einer Stelle "wissen" ob ein Taster gedrückt wurde, geht das mit einer einfachen if() Abfrage.

Die Funktion muss regelmäßig ausgeführt werden um den Taster auch abfragen zu können. Natürlich hab ich dann im "Hauptcode"(loop) irgendwo die Zeile(n):

if(buttonPressed()) startLEDblink(intervall);
LEDblink(); // Damit die LED auch weiterblinkt

Es ist mir eigentlich erst jetzt so richtig klar geworden, dass es wirklich eine andere Art zu Denken ist. :slight_smile:
Woher ich die Angewohnheit hab alles in Funktion (bzw. in php in Klassen) zu schreiben kann ich eigentlich gar nicht sagen. Ich denke, es hat mich einfach fasziniert und ich bin dabei geblieben.

ok, das mit den Funktionen geht bei kleinen Programmen eine Zeitlang noch gut aber bei größeren Projekten kann hier ein großes Chaos entstehen, nur durch die Variablen! Ich muss für mich "einfach" lernen richtige Klassen zu schreiben.

Grüße,
Donny

Kapselung als OOP-Gewinn, wo lokale Variablen in Funktionen doch sowas von "privat" sind?

Wo ist da mein Verständnisfehler?

Du kannst z.B. 2 Objekte (= Instanzen) einer Klasse haben, von denen jedes seine eigenen privaten (Member-)Variablen hat.
Mit lokalen Variablen in Funktionen funktioniert das nicht.

Gruß Tommy

Mit lokalen Variablen in Funktionen funktioniert das auch, aber nur, wenn diese nicht static sind.
Member-Variable eines Objekts behalten ihren Zustand auch nach Ende einer Methode dieser Klasse.

static Variable einer Funktion sind zwar auch "gekapselt", also nur von innerhalb der Funktion erreichbar, aber wenn diese Funktion mehrfach aufgerufen wird, arbeitet sie immer mit derselben Variablen.

Ok, dass die Inhalte der Membervariablen erhalten bleiben, hatte ich nicht erwähnt.
Das hatte ich leider im Verständnis vorausgesetzt.

Gruß Tommy

Ja, Member-Variable und lokale Variable innerhalb einer Funktion haben nicht viel miteinander zu tun.
Daher hatte ich ein Missverständnis vermutet.

Wenn allen alles klar ist, auch gut.

agmue:
Kapselung als OOP-Gewinn, wo lokale Variablen in Funktionen doch sowas von "privat" sind?

Wo ist da mein Verständnisfehler?

Ich verstehe deine Frage nicht!
OK, die Worte ja, aber nicht den Sinn.

Darum antworte ich auf das, was ich verstehe, und mit dem, was ich darüber weiß.

wo lokale Variablen in Funktionen doch sowas von "privat" sind?

Ganz so privat, wie du glaubst, sind sie nicht!

Nachweis (ebend aus meiner Wühlkiste gesucht):

#define WAECHTER(func) __attribute__((__cleanup__(func)))
 
void onVerwerf(int *value)
{
  Serial.println("lokaleVar wird verworfen ");
  Serial.print("Letzter Wert war: ");Serial.println(*value);
}

void test()
{

  int WAECHTER(onVerwerf) lokaleVar  = 42; // lokale Variable mit Beobachter
  
  lokaleVar++;
  Serial.print("lokaleVar ist: "); Serial.println(lokaleVar);
  lokaleVar++;
  Serial.print("lokaleVar ist: "); Serial.println(lokaleVar);
  lokaleVar++;
}



void setup() 
{
  Serial.begin(9600);      
  Serial.println("Start");

  Serial.println("Starte test()");

  test();
  
  Serial.println("test() Aufruf beendet");
}

void loop() 
{
}

Aber, das war nicht das, was du wissen wolltest, oder?

combie:
Ich verstehe deine Frage nicht!

Schade, aber ich starte nochmal einen Versuch.

Es geht ja um die Frage, was mich zum Umstieg auf OOP motivieren könnte. Wenn ich die letzten Beiträge richtig verstehe, dann ist die Kapselung ein wichtiges Argument. Also vergleiche ich mit der prozeduralen Funktion:

b = meineFunktion(a);

Meiner Funktion wird eine Kopie des Objekts a übergeben, die Funktion macht was damit und das Ergebnis kann ein Objekt b sein. Ich habe die Funktion getestet, danach kann ich ihren Inhalt vergessen, gerade so, wie Du es bei der Klasse beschrieben hast. Die Funktion ist gekapselt.

Ich kann diesbezüglich keinen Vorteil der Klasse gegenüber einer Funktion erkennen.

Wer nun mit Taschenspielertricks wie Zeigern daherkommt, der nutzt auch struct ohne private. Das kann nützlich sein, widerspricht aber dem Grundgedanken.

Was in #55 steht, weckt zwar einerseits meinen Widerspruch, führt mich aber andererseits auf den möglichen Kern: Wenn ich eine Funktion mehrfach aufrufe, muß eine statische Variable ein Feld sein, weshalb das übergebene Objekt auch einen Index enthalten muß. Diesen Index muß ich selbst pflegen.

Demgegenüber brauche ich mich um die Verwaltung der Instanzen einer Klasse nicht zu kümmern. Beispiel:

Blumen blumen[]  // Ein Feld der Struktur Blumen
{ // messPin, relaisPin, trockenWert, messIntervall, giessIntervall, nachgiessenIntervall
  {A1, 2, 200, 3000, 500, 5000},  // Werte für Blume und Pumpe 1
  {A2, 3, 250, 2500, 500, 5000}   // Werte für Blume und Pumpe 2
};

Ergänze ich hier eine Zeile, habe ich eine Blume mehr. Da freut sich meine Faulheit.

Konnte ich verdeutlichen, um was es mir geht?

Der Blick in Deine Wühlkiste ist durchaus amüsant und in anderem Zusammenhang möglicherweise auch hilfreich, aber bezogen auf dieses Thema doch eher ein Spaß, oder? Probiert habe ich es dennoch :slight_smile: