Informationen zu C ++ und objektorientierter Programmierung

Hallo Habr!



Wir möchten Ihre Aufmerksamkeit auf einen Artikel lenken, dessen Autor den rein objektorientierten Ansatz bei der Arbeit mit der C ++ - Sprache nicht gutheißt. Wir bitten Sie, wenn möglich nicht nur die Argumentation des Autors, sondern auch die Logik und den Stil zu bewerten.



Es hat eine Menge zu schreiben in letzter Zeit über C ++ und wo die Sprache geleitet wird und wie viel von dem, was „modernen C ++“ genannt wird , ist einfach keine Option für Spiele - Entwickler.



Obwohl ich diesen Standpunkt voll und ganz teile, neige ich dazu, die Entwicklung von C ++ als Ergebnis der Verankerung allgegenwärtiger Ideen zu betrachten, an denen sich die meisten Entwickler orientieren. In diesem Artikel werde ich versuchen, einige dieser Ideen zusammen mit meinen eigenen Gedanken zu organisieren - und vielleicht bekomme ich etwas Schlankes.



Objektorientierte Programmierung (OOP) als Werkzeug



Obwohl C ++ als Programmiersprache mit mehreren Paradigmen beschrieben wird, verwenden die meisten Programmierer in der Praxis C ++ nur als objektorientierte Sprache (generische Programmierung wird verwendet, um OOP zu "ergänzen").



OOP soll ein Werkzeug sein, eines von vielen Paradigmen, mit denen ein Programmierer Probleme im Code lösen kann. Nach meiner Erfahrung wird OOP jedoch von den meisten Fachleuten als Goldstandard für die Softwareentwicklung akzeptiert. Grundsätzlich beginnt die Entwicklung einer Lösung damit, zu bestimmen, welche Objekte wir benötigen. Die Lösung eines bestimmten Problems beginnt, nachdem der Code auf die Objekte verteilt wurde. Mit dem Übergang zu dieser Art von objektorientiertem Denken verwandelt sich OOP von einem Werkzeug in eine ganze Werkzeugkiste.



Über Entropie als geheime Kraft, die die Softwareentwicklung antreibt



Ich stelle mir eine OOP-Lösung gerne als Konstellation vor: Es ist eine Gruppe von Objekten mit zufällig gezeichneten Linien zwischen ihnen. Eine solche Lösung kann auch als Graph betrachtet werden, in dem Objekte Knoten sind und die Beziehungen zwischen ihnen Kanten sind, aber das Phänomen einer Gruppe / eines Clusters, das durch die Konstellationsmetapher vermittelt wird, ist mir näher (im Vergleich dazu ist der Graph zu abstrakt).



Aber ich mag es nicht, wie solche "Konstellationen von Objekten" zusammengesetzt sind. Nach meinem Verständnis ist jede solche Konstellation nichts anderes als eine Momentaufnahme des Bildes, das sich im Kopf des Programmierers gebildet hat und widerspiegelt, wie der Lösungsraum in einem bestimmten Moment aussieht. Selbst unter Berücksichtigung aller Versprechen, die im objektorientierten Design in Bezug auf Erweiterbarkeit, Wiederverwendbarkeit, Kapselung usw. gegeben werden, ist die Zukunft unvorhersehbar, sodass wir in jedem Fall eine Lösung für genau das Problem anbieten können, mit dem wir jetzt konfrontiert sind.



Wir sollten ermutigt werden, das unmittelbar vor uns liegende Problem „nur“ zu lösen, aber meiner Erfahrung nach schafft ein Programmierer, der Designprinzipien im Sinne von OOP verwendet, eine Lösung, während er sich auf die Annahme beschränkt, dass sich das Problem selbst nicht wesentlich ändern wird und Dementsprechend kann die Lösung als dauerhaft angesehen werden. Ich meine, von nun an beginnen die Leute, über die Lösung in Bezug auf die Objekte zu sprechen, die die oben genannte Konstellation bilden, und nicht in Bezug auf Daten und Algorithmen. Das Problem selbst ist abstrahiert.

Trotzdem unterliegt das Programm nicht weniger Entropie als jedes andere System, und daher wissen wir alle, dass sich der Code ändern wird. Darüber hinaus auf unvorhersehbare Weise. Aber für mich ist in diesem Fall absolut klar, dass sich der Code auf jeden Fall verschlechtert und in Chaos und Unordnung verfällt, wenn Sie ihn nicht bewusst bekämpfen.



Ich habe dieses Manifest in OOP-Lösungen auf viele verschiedene Arten gesehen:



  • In der Hierarchie erscheinen neue Zwischenebenen, die ursprünglich nicht eingeführt werden sollten.
  • Neue virtuelle Funktionen werden mit leeren Implementierungen in den meisten Hierarchien hinzugefügt.
  • Eines der Objekte in der Konstellation erfordert mehr Verarbeitung als geplant, wodurch die Verbindungen zwischen den anderen Objekten zu rutschen beginnen.
  • , , , .
  • .…


Dies sind alles Beispiele für falsch organisierte Erweiterbarkeit. Darüber hinaus ist das Ergebnis immer das gleiche, es kann in ein paar Monaten oder vielleicht in ein paar Jahren kommen. Mit Hilfe des Refactorings versuchen sie, Verstöße gegen die OOP-Entwurfsprinzipien zu beseitigen, die beim Hinzufügen neuer Objekte zur Konstellation und aufgrund der Neuformulierung des Problems selbst entstanden sind. Manchmal hilft Refactoring. Für einige Zeit. Die Entropie ist stabil und Programmierer haben keine Zeit, jede OOP-Konstellation umzugestalten, um sie zu überwinden. Daher befindet sich jedes Projekt regelmäßig in derselben Situation, deren Name Chaos ist.



Im Lebenszyklus eines OOP-Projekts kommt früher oder später ein Punkt, nach dem es unmöglich ist, es aufrechtzuerhalten. In der Regel sollte in einem solchen Moment eine von zwei Maßnahmen ergriffen werden:



  • « »: - . , , , , , .
  • : -, , , .


Bitte beachten Sie: Die Option mit einer Blackbox muss weiterhin neu geschrieben werden, falls die Entwicklung neuer Funktionen fortgesetzt werden muss und / oder die Notwendigkeit zur Beseitigung von Fehlern bestehen bleibt.



Die Situation beim Umschreiben einer Lösung bringt uns zurück zu dem Phänomen einer Momentaufnahme des verfügbaren Lösungsraums zu einem bestimmten Zeitpunkt. Was hat sich zwischen OOP Design # 1 und der aktuellen Situation geändert? Im Grunde ist es das. Das Problem hat sich geändert, daher ist eine andere Lösung erforderlich.



Während wir die Lösung nach den Prinzipien des OOP-Designs schrieben, abstrahierten wir das Problem, und sobald es sich änderte, zerfiel unsere Lösung wie ein Kartenhaus.

Ich denke, in diesem Moment beginnen wir darüber nachzudenken, was schief gelaufen ist. Wir versuchen, in die andere Richtung zu gehen und die Strategien zur Lösung des Problems auf der Grundlage der Ergebnisse des Postmortems (Nachbesprechung) zu aktualisieren. Jedes Mal, wenn ich auf ein solches Szenario zum "Umschreiben" stoße, ändert sich nichts: Es werden erneut OOP-Prinzipien verwendet, nach denen ein neuer Snapshot implementiert wird, der dem aktuellen Status des Problemraums entspricht. Der gesamte Zyklus wird wiederholt.



Einfache Codeentfernung als Konstruktionsprinzip



In jedem System, das auf dem Prinzip der OOP basiert, erhalten die Objekte in der "Konstellation" die Hauptaufmerksamkeit. Aber ich glaube, dass die Beziehungen zwischen Objekten genauso wichtig sind, wenn nicht sogar wichtiger als die Objekte selbst.



Ich bevorzuge einfache Lösungen, bei denen der Abhängigkeitsgraph des Codes aus der minimalen Anzahl von Knoten und Kanten besteht. Je einfacher die Lösung ist, desto einfacher ist es, sie nicht nur zu ändern, sondern auch zu entfernen. Ich fand auch heraus, dass je einfacher es ist, den Code zu entfernen, desto schneller können Sie die Lösung neu fokussieren und an sich ändernde Problembedingungen anpassen. Gleichzeitig wird der Code widerstandsfähiger gegen Entropie, da es viel weniger Aufwand erfordert, ihn in Ordnung zu halten und zu verhindern, dass er ins Chaos rutscht.



Über Leistung per Definition



Eine der wichtigsten Überlegungen zur Vermeidung des OOP-Designs ist jedoch die Leistung. Je mehr Code Sie ausführen müssen, desto schlechter ist die Leistung.



Es ist auch unmöglich, nicht zu bemerken, dass OOP-Funktionen per Definition nicht mit der Leistung glänzen. Ich habe eine einfache OOP-Hierarchie mit einer Schnittstelle und zwei abgeleiteten Klassen implementiert, die einen einzelnen reinen virtuellen Funktionsaufruf im Compiler-Explorer überschreiben .



Der Code in diesem Beispiel gibt entweder "Hallo Welt!" Aus oder nicht, abhängig von der Anzahl der an das Programm übergebenen Argumente. Anstatt alles, was ich gerade beschrieben habe, direkt zu programmieren, wird eines der Standard-OOP-Entwurfsmuster, die Vererbung, verwendet, um dieses Problem im Code zu lösen.



In diesem Fall ist das Auffälligste, wie viel Code-Compiler auch nach der Optimierung generieren. Wenn Sie genau hinschauen, können Sie sehen, wie teuer und gleichzeitig nutzlos eine solche Wartung ist: Wenn eine Anzahl von Argumenten ungleich Null an das Programm übergeben wird, weist der Code weiterhin Speicher (Aufruf new) zu, lädt die Adressen vtablebeider Objekte, lädt die Adresse der Funktion Work()für ImplBund springt darauf, so dass dann sofort kehre zurück, da es dort nichts zu tun gibt. Schließlich wird es aufgerufen delete, den zugewiesenen Speicher freizugeben.



Keine dieser Operationen war überhaupt notwendig, aber der Prozessor führte sie alle ordnungsgemäß aus.



Wenn eines der Hauptziele Ihres Produkts das Erreichen einer hohen Leistung ist (seltsam, wenn es anders wäre), sollten Sie im Code unnötige kostspielige Vorgänge vermeiden, einfache Vorgänge bevorzugen, die leicht zu beurteilen sind, und Konstrukte verwenden, die zur Erreichung dieses Ziels beitragen.



Nehmen wir zum Beispiel die Einheit . Als Teil ihrer jüngsten Praxis ist Leistung Korrektheit mit C #, einer objektorientierten Sprache, da diese Sprache bereits in der Engine selbst verwendet wird. Sie entschieden sich jedoch für eine Teilmenge von C # , die nicht fest an OOP gebunden ist, und erstellen auf ihrer Grundlage Konstrukte, die für eine hohe Leistung geschärft wurden.



Angesichts der Aufgabe eines Programmierers, Probleme mit einem Computer zu lösen, ist es undenkbar, dass unser Unternehmen dem Schreiben von Code so wenig Aufmerksamkeit widmet, dass der Prozessor tatsächlich die Arbeit erledigt, in der der Prozessor besonders gut ist.



Über den Kampf gegen Stereotypen



In Angelo Pesces Artikel " Überkomplikation ist die Wurzel allen Übels " trifft der Autor ins Schwarze (siehe letzter Abschnitt: Menschen), indem er zugibt, dass die meisten Softwareprobleme tatsächlich menschliche Faktoren sind.



Die Mitarbeiter des Teams müssen interagieren und ein gemeinsames Verständnis dafür entwickeln, was das übergeordnete Ziel ist und wie sie es erreichen können. Wenn es im Team Meinungsverschiedenheiten gibt, zum Beispiel über den Weg zum Ziel, ist es für weitere Fortschritte notwendig, einen Konsens zu entwickeln. Dies ist normalerweise nicht schwierig, wenn die Meinungsverschiedenheiten gering sind, aber es ist viel schwieriger zu tolerieren, wenn sich die Optionen grundlegend unterscheiden, sagen wir "OOP oder nicht OOP".

Ihre Meinung zu ändern ist nicht einfach. Es ist schwer und schmerzhaft, an Ihrem Standpunkt zu zweifeln, zu erkennen, wie falsch Sie waren, und Ihren Kurs anzupassen. Aber es ist viel schwieriger, die Meinung eines anderen zu ändern!



Ich hatte viele Gespräche mit verschiedenen Leuten über OOP und seine inhärenten Probleme, und obwohl ich glaube, dass ich immer erklären konnte, warum ich so denke und nicht anders, glaube ich nicht, dass ich es geschafft habe, jemanden von OOP abzuwenden.



Zwar habe ich im Laufe der Jahre der Arbeit drei Hauptargumente für mich identifiziert, aufgrund derer die Menschen nicht bereit sind, der anderen Seite eine Chance zu geben:



  • « ». « ». « » . , , ( , - ). « …».
  • « , , , ». «» , , . , « ».
  • "Jeder kennt OOP. Es ist sehr praktisch, mit Menschen in einer gemeinsamen Sprache zu sprechen, die über allgemeine Kenntnisse verfügen." Dies ist ein logischer Fehler, der als "Argument an die Menschen" bezeichnet wird. Wenn also fast alle Programmierer die Prinzipien von OOP verwenden, kann diese Idee nicht unangemessen sein.


Mir ist völlig bewusst, dass das Aufdecken logischer Fehler in der Argumentation nicht ausreicht, um sie zu entlarven. Ich glaube jedoch, dass Sie, wenn Sie die Fehler in Ihren eigenen Urteilen sehen, der Wahrheit auf den Grund gehen und den tiefen Grund finden können, warum Sie eine ungewöhnliche Idee ablehnen.



All Articles