C++ Start, gleich mit oop? <erledigt>



  • Ich habe in letzter Zeit vermehrt mit Entwicklern zu tun, die z.B. von einer funktionalen bzw. prozeduralen Programmiersprache auf C++ umsteigen wollen bzw. müssen.
    Ab und an hapert es, den Unterschied von globalen Funktionen, die ich i.d.R. einfach aufrufen kann und Methoden einer Klasse, die ich i.d.R. nur bei instantiierten Objekten aufrufen kann, zu verinnerlichen.
    Ich vermisse oft den Willen, C++ mittels eines Handbuches "von der Pike an" zu lernen. Ich habe mir einige Tutorials angeschaut und kann den Frust verstehen. Es geht los mit dem bekannten, ausgelutschten "Hello world" und geht dann über, in seitenweise klein, klein Erklärungen.
    Das ist für Einsteiger gut, aber für genannte Zielgruppe zu viele Wiederholungen.

    Ich habe versucht, den Start einer Einführung in C++ mal anders zu machen, ich fange gleich mit einer Klasse an. Mein Ansinnen ist es, das Interesse an der OOP mit dem Aufzeigen von Klasse und Objekt sofort zu wecken. Dann kann der geneigte C++ Entwickler sich gerne selbst peu à peu seine nötigen Infos suchen.

    Über eine kurze Kritik, ob das so zu gebrauchen ist, würde ich mich freuen.

    https://www.globalobjects.de/Written/OOPCPP/index.html



  • Hallo,
    ich habe deinen Artikel mal überflogen.

    Persönlich finde ich gerade deine erste Klasse Adressat kein gutes Einstiegsbeispiel, da daraus nicht wirklich ersichtlich wird, warum man nicht einfach std::string name benutzt (und warum man dann soviel Code dazu schreiben sollte). Gerade für das Neusetzen des Namens (setName) sollte erklärt werden, warum man überhaupt eine Extra-Funktion dafür benutzt (Stichwort: Invariante) bzw. ob man diese Funktion benötigt, d.h. diese überhaupt dem OOP-Gedanken (Stichwort: Kapselung) entspricht.

    Eine Klasse sollte so erklärt werden, daß sie eine reine Datenstruktur (wie man sie aus C kennt) mit sinnvollen Funktionen und Operatoren verbindet (und nicht bloß reine Getter und Setter).

    Edit:
    Vllt. solltest du mit einem guten C++ Beispielcode (d.h. dem Benutzen einer Klasse in der main-Funktion) als Teaser beginnen und dann nach und nach erklären, was man dazu benötigt, um daraus den Code für diese Klasse zu entwickeln. Evtl. sogar ein Vergleich zum prozeduralen Code dafür, so wie es auch in Learn C++: 14.2 — Introduction to classes gemacht wird.



  • @Th69
    Herzlichen Dank für Deine Anregungen, werde ich beim nächsten Entwurf, wenn es denn einen gibt, berücksichtigen.



  • @Helmut-Jakoby

    eine Klasse entspricht einer Anwendung (z.B. Terminkalender). Die Klasse definiert sämtliche Methoden welche die Anwendung erfordert (z.B. Termin eintragen). Ausgeführt werden diese Methoden durch eine Instanz der Klasse. Klasseninstanzen sind Objekte die Anwendungsdaten als Eigenschaften transportieren. Damit sind Klasseninstanzen auch abstrakte Datentypen deren Methoden den Zugriff auf die Daten beschreiben (string, array, iterator...).

    Schönen Sonntag



  • @_ro_ro
    Danke, interessant zusammengefasst.



  • @Helmut-Jakoby

    danke zurück. Heutige Programmierer neigen dazu, ihre Programme mit Objekten zu überfluten. wer 5 Klassen und deren Instanzen braucht um einen einzigen HTTP-Request zu feuern hat den Sinn einer objektorientierten Programmierung ganz sicher nicht verstanden. Und ja, das gibt es 😉

    Viele Grüße



  • @_ro_ro sagte in C++ Start, gleich mit oop?:

    @Helmut-Jakoby

    danke zurück. Heutige Programmierer neigen dazu, ihre Programme mit Objekten zu überfluten. wer 5 Klassen und deren Instanzen braucht um einen einzigen HTTP-Request zu feuern hat den Sinn einer objektorientierten Programmierung ganz sicher nicht verstanden. Und ja, das gibt es 😉

    Viele Grüße

    Ich bin der gleichen Meinung ... Ein Kollege hatte das auch so gemacht, und ich hab ihn nicht wieder gesehen.

    Schönen Montag



  • @_ro_ro Neee, ich glaube, du bist auf dem Holzweg:

    • Single Responsible Principle - Jede Klasse ist für genau eine Sache zuständig.

    Ich habe erst gedacht, ich hätte "Anwendung" falsch verstanden, mir scheint, wir haben ein grundlegend Unterschiedliches Verständnis von OOP.
    Zum Beispiel, ist ein "Vektor" eine Anwendung?

    Viele Leute neigen eher dazu, ihre Klassen zu groß werden zu lassen und Zuständigkeiten zu vermischen, was zu unwartbaren und untestbaren Code führt.
    Zu kleine Klassen habe ich hingegen noch nicht gesehen.

    Zu viele Objekte? Um es mit Alan Kay zu sagen "Everything is an object"

    Eine Klasse definiert einen Datentyp und Algorithmen die auf dem Datentyp operieren. Darüber hinaus sollte sicherstellen, immer in einem definierten Zustand zu sein und ein Interface bereitstellen, über die der Zustand der von der Klasse erstellten Objekte verändert werden kann.

    @Helmut-Jakoby Wenn ich ein C++ Tutorial schreiben würde, würde ich vermutlich damit anfangen ein Unittest Framework einzuführen, damit man die Beispiele testen kann ohne cout in main.
    Dann finde ich die Mischung Deutsch/Englisch im Code nicht schön. Zum Beispiel: Adressatbank::getAdressat.

    Mehr fachlich zu dem Code:
    Deine Adressatbank ist irgendwie ein Singleton. Abgesehen davon, dass ich per se kein Freund von Singletons bin, würde ich, wenn es schon sein muss, zur Implementierung von Scott Meyer tendieren. Da ich gerade die Originalquelle nicht finde: https://www.modernescpp.com/index.php/creational-patterns-singleton/

    Dann nutzt du virtuelle Destruktoren (mit gutem Grund, wegen Vererbung), aber ohne ein Wort über Rule of zero / Rule of five zu verlieren und die entsprechenden Sachen zu implementieren. Mit dem virtuellen Destruktor werden der Move Ctor und Assignment Operator nicht vom Compiler zur Verfügung gestellt und es werden überall Kopien gemacht, wo sonst gemoved werden könnte. Gerade das ist eine der trickreichen Sachen in C++ (https://scottmeyers.blogspot.com/2014/03/a-concern-about-rule-of-zero.html).

    Dann, als letztes zum Code noch: Du nutzt shared_ptr ohne das Konzept von Ownership zu erklären. Was mich auch direkt zu der Frage bring, warum GetAdressat einen shared_ptr zurück gibt. Das ist eine der Stellen, an denen ich wohl einen "rohen" Zeiger rausgegeben hätte. Dann ist nur der Aufrufer dafür verantwortlich, den nur solange zu nutzen, solange der Pointer auf einen gültigen Adressat zeigt.

    Insgesamt frage ich mich bei OOP Tutorials häufig: Muss das wirklich mit Vererbung gelöst werden. Mein Eindruck ist, dass OOP häufig an und mit Java erklärt wird und man die erlernten Muster auf C++ überträgt. In C++ heißt das, es gibt eine Indirektion über vtables, die natürlich irgendwie einen Einfluss haben auf Performance. Desweiteren finde ich, Vererbung auch immer etwas anstrengend zu lesen: In dem Fall "Eine Person ist ein Adressat". Ich finde es über Komposition auch in dem Fall schöner Ausgedrückt: Ein Adressat kann eine Person oder eine Organisation sein:

    class Adressat
    {
       std::variant<Person, Organisation> m_adressat;
    }
    

    Sorry für die viele Kritik. Ich hoffe du siehst es als Anregung zum drüber nachdenken 😉
    Es gibt einfach wenige Tutorials, die modernes C++ gut erklären. Vlt hatte ich einfach etwas anderes erwartet. Ich selbst schreibe sowas ja selbst auch nicht, weil ich nicht weiß, wo ich anfangen sollte und wie man das didaktisch gut aufbaut 😃



  • @Schlangenmensch sagte in C++ Start, gleich mit oop?:

    Ich habe erst gedacht, ich hätte "Anwendung" falsch verstanden, mir scheint, wir haben ein grundlegend Unterschiedliches Verständnis von OOP.
    Zum Beispiel, ist ein "Vektor" eine Anwendung?
    Viele Leute neigen eher dazu, ihre Klassen zu groß werden zu lassen und Zuständigkeiten zu vermischen, was zu unwartbaren und untestbaren Code führt.
    Zu kleine Klassen habe ich hingegen noch nicht gesehen.
    Zu viele Objekte? Um es mit Alan Kay zu sagen "Everything is an object"
    Eine Klasse definiert einen Datentyp und Algorithmen die auf dem Datentyp operieren. Darüber hinaus sollte sicherstellen, immer in einem definierten Zustand zu sein und ein Interface bereitstellen, über die der Zustand der von der Klasse erstellten Objekte verändert werden kann.

    Alles richtig hier. Allerdings möchte ich noch zu bedenken geben, dass unterschiedliche Programmiersprachen zu unterschiedlichen Stilen führen. Während ich in einer Java-App tendenziell sehr viele Klassen habe, habe ich in meinen Python-Apps deutlich weniger Klassen. Und C++ liegt für mich irgendwo dazwischen.

    Ich halte wenig davon, eine Klasse zu schreiben, um dann vielleicht nur eine Member-Funktion zu haben. Dann kann ich den State auch gleich als weiteren Parameter übergeben. Zum Beispiel durch das Duck-Typing in Python und Template-Funktionen in C++ kann man sehr generelle Funktionen schreiben. Man braucht nicht immer für alles Klassen. Und bei größeren Apps kann man dann z.B. Dependency Injection nutzen, um die Anwendung letztendlich zusammenzubauen anstelle davon, dass man ein großes globales "Anwendungsobjekt" hat, das alle möglichen unzusammenhängenden Einstellungen kennt.

    Ich finde jedenfalls Anwendungen, bei denen alles 10 Klassen tief verschachtelt ist, schwer zu lesen und zu warten. Flacher ist oft besser. (Natürlich nicht immer, kann ja auch alles mal gerechtfertigt sein).



  • @wob

    der Begriff "dependency injection" suggeriert Abhängigkeiten. Tatsächlich sind Abhängigkeiten aber das was man eher nicht haben will. Meiner Erfahrung nach ist es besser, Methoden fremder Klassen zu delegieren. Hierzu bekommt die auszuführende Instanz eine Instanz der Target-Class als Eigenschaft. Damit werden Methoden fremder Klassen zu eigenen Methoden gemacht.

    Und was den Klassenentwurf betrifft: Gerade in c++ sollte man sehr genau überlegen, ob man das Erbe einer Legacy-Class antritt, das wurde hier auch schon diskutiert (tag: virtual destructor). Also man sollte die Klasse von der man zu erben beabsichtigt schon sehr genau kennen. Auch ein Grund mehr sich für eine Delegation zu entscheiden.

    mfg



  • @_ro_ro Ja, wenn man deinen Vorschlag naiv implementiert hat man aber eine feste Abhängigkeit zwischen "Auszuführende" Instanz und "Target". Um diese Abhängigkeit aufzuweichen, gibt es das Prinzip der "Dependency Injection" (Übrigens der Name eines Design Pattern)
    Durch die Umsetzung dieses Patterns werden Abhängigkeiten nicht fest in der Klasse implementiert, sondern werden als Parameter mit übergeben, zum Beispiel im Konstruktor. Hierfür definiert man ein Interface, welches dann von der tatsächlichen Klasse implementiert werden muss.
    Dann hängt die Klasse nicht mehr von der tatsächlichen Implementierung ihrer Komponenten ab und man kann diese für Testfälle einfach duch Mocks ersetzen. Oder durch eine andere Implementierung, oder was auch immer.
    Dank Templates brauchen wir in C++ für die Interfaces auch keine klassiche runtime Polymorphy.



  • @_ro_ro: Du scheinst nicht verstanden haben, was DI bedeutet, denn du hast eine der Möglichkeiten beschrieben, wie man Dependency Injection umsetzen kann.



  • @Th69 sagte in C++ Start, gleich mit oop?:

    @_ro_ro: Du scheinst nicht verstanden haben, was DI bedeutet, denn du hast eine der Möglichkeiten beschrieben, wie man Dependency Injection umsetzen kann.

    Doch das habe ich sehr gut verstanden. Es kommt nämlich darauf an wie man es macht, also ob man eine Abhängigkeit erzeugt oder nicht.

    MFG



  • Hallo @Schlangenmensch ,
    danke für Deine Gedanken zu diesem Thema.

    Ein Unittest Framework als ersten Schritt einzuführen, halte ich für zu "schwergewichtig", muss mal schauen, ob ich das im Anhang unterbringe.

    Das Singleton ist mir bekannt (im verlinkten Absatz verursacht MySingleton memory-leaks 😉 ), wollte aber vereinfacht nur den Aufruf von statischen Methoden zeigen ohne ein Pattern in der ersten Phase vorstellen zu müssen. Zumal Singletons ja wie im verlinkten Text erwähnt, "getarnte Statics" sind. Aber auch da werde ich einen Hinweis im Anhang einbauen, zumal das Thema "Parallelität" und "Threadsicherheit" ab C++11 durch "Meyers Singleton" gewährleistet ist.

    Das mit den virtualen Destruktor werde ich wohl besser erklären und dann im Anhang mit ein paar Worten auf Rule of * eingehen. Ich werde den Anhang sowieso erweitern. Wollte aber nicht viel weitergehen, als die ersten Gehversuche mit erstellen von Objekten zu begleiten und dann auf andere Tutorials verweisen. Ist doch eh schon alles mehrfach beschrieben. Nur der Start halt...

    Danke, gerade hier werde ich doch etwas genauer auf die Motivation eingehen, warum ich in diesem Fall shared_ptrempfehle, damit eben nicht der Aufrufer dafür verantwortlich ist, wann der gelieferte Adressat 'zerstört' wird. Man könnte ja z.B. erwähnen, dass die Methode static std::shared_ptr<Adressat> Adressatbank::getAdressat( std::size_t position ) irgendwann threadsicher gemacht wird und dann aus Threads aufgerufen werden kann. Dann könnte man sich ggf. auch über Ownership auslassen 🤔 .

    Mit der Vererbung gebe ich Dir recht, ich werde mehr auf den Aspekt "Vererbung versus Aggregation" eingehen.

    Zu guter Letzt, ich sehe Deine "Kritik" wirklich als Anregung, darum hatte ich ja gebeten.



  • Hallo,
    danke für Eure konstruktive Kritik.
    Ich habe das kleine Projekt aufgegeben.
    Btw; kann man den Thread als <erledigt> kennzeichnen?



  • @Helmut-Jakoby sagte in C++ Start, gleich mit oop?:

    Btw; kann man den Thread als <erledigt> kennzeichnen?

    Nö, nur das Ändern der Überschrift ist ggf. möglich.


Anmelden zum Antworten