Objekte versus Datenstrukturen

In dem Artikel, dessen Übersetzung unten vorgeschlagen wird, scheint Robert Martin mit Gedanken zu beginnen, die denen sehr ähnlich sind, die in Jegor Bugaenkos Diskussionen über ORM zu sehen sind, aber andere ziehen Schlussfolgerungen. Persönlich beeindruckt mich Yegors Herangehensweise, aber ich denke, dass Martin das Thema ausführlicher aufdeckt. Es scheint mir, dass es sich lohnt, alle kennenzulernen, die jemals darüber nachgedacht haben, welchen Platz ORM einnehmen soll und generell, warum wir Objekte brauchen, in denen alle Felder offen sind. Der Artikel ist im Genre "Dialog" geschrieben, in dem ein erfahrener Programmierer ein Problem mit jemandem bespricht, der weniger Erfahrung hat.


Was ist eine Klasse?

Eine Klasse ist eine Spezifikation vieler ähnlicher Objekte.


Was ist ein Objekt?

Ein Objekt ist eine Reihe von Funktionen, die Aktionen mit gekapselten Daten ausführen.


Oder ist es besser zu sagen, dass ein Objekt eine Reihe von Funktionen ist, die Aktionen mit Daten ausführen, deren Existenz impliziert ist

Im Sinne von "impliziert"?


Sobald das Objekt Funktionen hat, kann davon ausgegangen werden, dass sich dort auch Daten befinden, es jedoch keinen direkten Zugriff auf die Daten gibt und sie von außen überhaupt nicht sichtbar sind.

Sind die Daten nicht im Objekt?


Vielleicht sind sie es, aber die Regel, die besagt, dass sie da sein müssen, ist nicht. Aus Sicht des Benutzers ist ein Objekt nichts anderes als eine Reihe von Funktionen. Die Daten, mit denen diese Funktionen arbeiten, müssen existieren, aber die Position dieser Daten ist dem Benutzer unbekannt.

Sagen wir mal.


Was ist eine Datenstruktur?

Eine Datenstruktur ist eine Sammlung verwandter Elemente.


Mit anderen Worten, eine Datenstruktur ist eine Menge von Elementen, mit denen Funktionen arbeiten, deren Existenz implizit impliziert wird.

Okay, okay. Ich verstehe. Die Funktionen, die mit Datenstrukturen arbeiten, sind nicht in diesen Strukturen definiert, aber aus der Existenz einer Datenstruktur können wir schließen, dass es etwas geben muss, das mit ihnen funktioniert.


Richtig. Und was ist mit diesen beiden Definitionen?

In gewissem Sinne sind sie Gegensätze.


Wirklich. Sie ergänzen sich. Wie eine Hand und ein Handschuh.
  • Ein Objekt ist eine Reihe von Funktionen, die mit Datenelementen arbeiten, deren Existenz implizit impliziert wird.
  • Eine Datenstruktur ist eine Menge von Datenelementen, mit denen Funktionen arbeiten, deren Existenz implizit impliziert wird.

Wow! Es stellt sich also heraus, dass Objekte und Datenstrukturen nicht dasselbe sind!


Richtig. Datenstrukturen sind DTOs.

Und Tabellen in Datenbanken sind auch keine Objekte, oder?


Wieder wahr. Datenbanken enthalten Datenstrukturen, keine Objekte.

Warte einen Moment. Ordnet ORM nicht Tabellen aus der Datenbank Objekten zu?


Natürlich nicht. Sie können keine Datenbanktabellen Objekten zuordnen. Tabellen in der Datenbank sind Datenstrukturen, keine Objekte.

Was macht ORM dann?


Sie übertragen Daten von einer Struktur in eine andere.

Sie haben also nichts mit Objekten zu tun?


Gar nichts. Genau genommen gibt es so etwas wie ORM im Sinne einer Technologie, die relationale Daten auf Objekte abbildet, nicht, weil Tabellen nicht von einer Datenbank auf Objekte abgebildet werden können.

Aber sie sagten mir, dass ORMs Geschäftsobjekte sammeln.


Nein, ORM ruft Daten aus einer Datenbank ab, mit der Geschäftsobjekte arbeiten

Aber fallen diese Datenstrukturen nicht in Geschäftsobjekte?


Vielleicht bekommen sie oder vielleicht nicht. ORM weiß nichts darüber.

Der Unterschied ist jedoch rein semantisch.


Ja Nein. Es gibt weitreichende Konsequenzen.

Zum Beispiel?


Zum Beispiel den Entwurf des Datenbankschemas und den Entwurf von Geschäftsobjekten. Geschäftsobjekte definieren das Geschäftsverhalten. Das Datenbankschema definiert die Geschäftsdatenstruktur. Diese Strukturen sind durch sehr unterschiedliche Kräfte begrenzt. Eine Geschäftsdatenstruktur ist nicht unbedingt die beste Struktur für das Geschäftsverhalten.

Eeee. Das ist unverständlich.


Denken Sie so darüber nach. Das Datenschema ist nicht für eine einzelne Anwendung konzipiert, sondern für die unternehmensweite Verwendung vorgesehen. Daher ist die Datenstruktur ein Kompromiss zwischen mehreren verschiedenen Anwendungen.

Das ist klar.


Gut Denken Sie jetzt über jede einzelne Anwendung nach. Das Objektmodell jeder Anwendung beschreibt, wie das Verhalten der Anwendung strukturiert ist. Jede Anwendung verfügt über ein eigenes Objektmodell, um dem Verhalten der Anwendung besser zu entsprechen.

Ahh, ich verstehe. Da das Datenschema einen Kompromiss zwischen verschiedenen Anwendungen darstellt, hängt das Schema nicht vom Objektmodell jeder einzelnen Anwendung ab.


Richtig! Objekte und Strukturen sind auf verschiedene Dinge beschränkt. Sie passen sehr selten zusammen. Die Leute nennen dies die Nichtübereinstimmung der objektrelationalen Impedanz.

Etwas, an das ich mich erinnere. Es scheint jedoch, dass die Impedanzfehlanpassung nur mit ORM korrigiert wurde.


Und jetzt wissen Sie, dass dies nicht so ist. Impedanzfehlanpassung zwischen Objekten und Datenstrukturen ist komplementär und nicht isomorph.

Was denn


Sie sind Gegensätze, nicht etwas Ähnliches.

Gegensätze?


Ja, in einem sehr interessanten Sinne. Sie sehen, Objekte und Datenstrukturen implizieren diametral entgegengesetzte Kontrollstrukturen.

Was denn


Stellen Sie sich eine Reihe von Klassen vor, die eine Art gemeinsames Interface implementieren. Stellen Sie sich beispielsweise Klassen vor, die zweidimensionale Figuren darstellen, in denen Funktionen zum Berechnen der Fläche und des Umfangs einer Figur vorhanden sind.

Inwieweit werden Objekte in allen Beispielen von Shapes codiert?


Betrachten wir zwei verschiedene Arten von Formen: Quadrate und Kreise. Es ist klar, dass die Funktionen zur Berechnung der Fläche und des Umfangs dieser Klassen unterschiedliche Datenstrukturen verwenden. Es versteht sich auch, dass diese Operationen unter Verwendung eines dynamischen Polymorphismus aufgerufen werden.

Entschleunigen Sie bitte, nichts ist klar.


Es gibt zwei verschiedene Funktionen zur Berechnung der Fläche, eine für das Quadrat und eine für den Kreis. Wenn eine Funktion aufgerufen wird, um die Fläche eines bestimmten Objekts zu berechnen, entscheidet dieses Objekt, welche bestimmte Funktion aufgerufen werden soll. Dies nennt man dynamischen Polymorphismus.

Ok Natürlich. Ein Objekt weiß, wie seine Methoden implementiert sind. Natürlich.


Lassen Sie uns diese Objekte nun in Datenstrukturen umwandeln. Wir benutzen diskriminierte Gewerkschaften.

Was diskriminiert?


Diskriminierte Gewerkschaften. Nun, C ++, Zeiger, das Schlüsselwort union, ein Flag zur Bestimmung des Strukturtyps, Diskriminierte Gewerkschaften. In unserem Fall sind dies nur zwei unterschiedliche Datenstrukturen. Eine für den Platz und eine für den Kreis. Der Kreis hat einen Mittelpunkt und einen Radius. Und ein Typcode, aus dem hervorgeht, dass es sich um einen Kreis handelt.

Das Feld mit dem Code wird enum sein?


Na ja Und das Quadrat wird den oberen linken Punkt und die Länge der Seite haben. Und auch enum, um den Typ anzugeben.

Ok Es wird zwei Strukturen mit einem Typcode geben.


Richtig. Betrachten wir nun die Funktion für den Bereich. Es wird wahrscheinlich einen Schalter geben, oder?

Na ja Natürlich für zwei Klassen. Der Zweig für den Platz und für den Kreis. Und für den Perimeter benötigen Sie auch einen ähnlichen Schalter.


Und wieder richtig. Denken Sie nun über diese beiden Szenarien nach. In einem Szenario mit Objekten sind zwei Implementierungen von Funktionen für einen Bereich unabhängig voneinander und gehören (gewissermaßen) direkt zum Typ. Die Funktion für die Fläche des Quadrats gehört zum Quadrat, und die Funktion zum Bestimmen der Fläche des Kreises gehört zum Kreis.

Okay, ich verstehe, wohin du führst. In einem Szenario mit Datenstrukturen befinden sich beide Implementierungen einer Funktion für einen Bereich in derselben Funktion und gehören nicht zum Typ (was auch immer dieses Wort bedeutet).


Weiter ist besser. Welcher Code sollte bei Objekten geändert werden, wenn Sie den Typ "Dreieck" hinzufügen müssen?

Ändern Sie überhaupt nichts. Erstelle einfach eine neue Triangle-Klasse. Obwohl dies nicht der Fall ist, müssen Sie wahrscheinlich den Code korrigieren, mit dem die Objekte erstellt werden.


Richtig. Wenn Sie also einen neuen Typ hinzufügen, sind die Änderungen vernachlässigbar. Angenommen, Sie müssen eine neue Funktion hinzufügen, z. B. eine Funktion zum Bestimmen des Mittelpunkts.

Dann müssen Sie es zu allen drei Typen, Kreis, Quadrat und Dreieck, hinzufügen.


Gut Es stellt sich heraus, dass das Hinzufügen neuer Funktionen schwierig ist, da Sie in jeder Klasse Änderungen vornehmen müssen.

Bei Datenstrukturen ist alles anders. Um ein Dreieck hinzuzufügen, müssen Sie jede Funktion ändern, um Zweige hinzuzufügen, die das Dreieck in jedem Schalter behandeln.


Richtig. Es ist schwierig, Typen hinzuzufügen, Sie müssen jede Funktion bearbeiten.

Um jedoch eine Funktion für die Zentrale hinzuzufügen, muss nichts geändert werden.


Das Hinzufügen von Funktionen ist einfach.

Wow. Es stellt sich heraus, dass diese beiden Ansätze direkt entgegengesetzt sind.


Auf jeden Fall ja. Um es zusammenzufassen

  • Es ist schwierig, Klassen neue Funktionen hinzuzufügen. Sie müssen in jeder Klasse Änderungen vornehmen
  • Das Hinzufügen neuer Funktionen zu Datenstrukturen ist einfach. Sie müssen lediglich eine Funktion hinzufügen, nichts anderes muss geändert werden
  • Das Hinzufügen neuer Typen zu Klassen ist einfach. Sie müssen lediglich eine neue Klasse hinzufügen
  • Es ist schwierig, neue Typen für Strukturen hinzuzufügen, Sie müssen jede Funktion reparieren

Ja Gegensätze. Gegensätze im eigentümlichen Sinne. Das heißt, wenn im Voraus bekannt ist, dass neue Funktionen hinzugefügt werden müssen, ist es zweckmäßig, Datenstrukturen zu verwenden. Wenn Sie jedoch im Voraus wissen, dass Sie neue Typen hinzufügen müssen, müssen Sie Klassen verwenden.


Gute Beobachtung! Aber heute müssen wir uns noch eine Sache überlegen. Es gibt einen weiteren Punkt, an dem Datenstrukturen und Klassen einander gegenüberstehen. Abhängigkeiten.

Sucht?


Ja, die Richtung der Abhängigkeiten im Quellcode.

Okay, ich werde fragen. Was ist der unterschied


Schauen wir uns den Fall der Strukturen an. Jede Funktion enthält einen Schalter, der die gewünschte Implementierung basierend auf dem Typcode in der Union auswählt.

Ja, das ist es. Na und?


Schauen wir uns den Funktionsaufruf für den Bereich an. Der aufrufende Code hängt von der Funktion für das Gebiet ab, und die Funktion für das Gebiet hängt von jeder spezifischen Implementierung ab.

Und was meinst du mit "abhängig"?


Stellen Sie sich vor, dass jede Implementierung einer Funktion für einen Bereich einer separaten Funktion zugeordnet ist. Das heißt, es gibt circleArea-, squareArea- und triangleArea-Funktionen.

Nun, es stellt sich heraus, dass es in den Vermittlungszweigen einfach Aufrufe zu diesen Funktionen geben wird.


Stellen Sie sich vor, diese Funktionen befinden sich in verschiedenen Dateien.

Dann in die Datei mit dem Schalter importieren oder für Dateien mit Funktionen verwenden oder einschließen.


Richtig Dies ist eine Abhängigkeit auf Quellcodeebene. Eine Quelle hängt von einer anderen Quelle ab. Wie ist diese Abhängigkeit gerichtet?

Der Quellcode mit Schalter hängt vom Quellcode ab, in dem sich die Implementierungen befinden.


Was ist mit dem Code, der die Funktion für den Bereich aufruft?

Der aufrufende Code hängt vom Code mit Schalter ab, der von allen Implementierungen abhängt.


Richtig. In allen Quellen ist der Pfeil in die Richtung des Aufrufs gerichtet, vom aufrufenden Code bis zur Implementierung. Wenn Sie also eine kleine Änderung an diesen Implementierungen vornehmen möchten ...

Okay, okay, ich verstehe, worauf du hinaus willst. Eine Änderung in einer der Implementierungen führt zur Neukompilierung aller Dateien mit switch, und dies führt dazu, dass alles, was diesen switch aufruft, neu kompiliert wird, beispielsweise in unserem Fall die Funktion für area.


Ja Zumindest für Sprachen, die Änderungsdaten verwenden, um zu verstehen, was neu erstellt werden muss.

Und das sind im Allgemeinen alle Systeme mit statischer Typisierung, oder?


Ja und einige andere Systeme ohne

Dies muss viel umgebaut werden.


Und viel zu wiederholen.

Okay, aber im Fall von Klassen ist es umgekehrt?


Ja, da der Code, der die Funktion für den Bereich aufruft, von der Schnittstelle abhängt und die Implementierung auch von dieser Schnittstelle abhängt.

Ich verstehe Der Code für die Square-Klasse importiert oder verwendet oder enthält eine Datei mit der Shape-Schnittstelle.


Richtig. Der Pfeil in den Implementierungsdateien zeigt in die entgegengesetzte Richtung zum Aufruf. Es wird vom Implementierungscode zum Aufrufcode weitergeleitet. Zumindest wird dies für statisch typisierte Sprachen der Fall sein. Bei dynamisch getippten Sprachen ist der Code, der die Funktion für den Bereich aufruft, von nichts abhängig, da die Verknüpfung zur Laufzeit erfolgt.

Ja ok Das heißt, wenn Sie Änderungen an einer der Implementierungen vornehmen ...


Es ist nur erforderlich, den Code mit diesen Änderungen neu zu erstellen und zu installieren.

Dies liegt daran, dass Abhängigkeiten entgegen der Anrufrichtung gerichtet sind.


Ja, wir nennen es Abhängigkeitsinversion.

Okay, lassen Sie uns alles zusammenfassen. Klassen und Datenstrukturen stehen sich in dreifacher Hinsicht gegenüber.


  • Die Funktionen befinden sich explizit in den Klassen, und Sie können nur raten, ob Daten vorhanden sind. Datenstrukturen sind explizit in den Datenstrukturen vorhanden, und Sie können nur raten, welche Funktionen verfügbar sind.
  • Bei Klassen ist das Hinzufügen von Typen einfach, das Hinzufügen von Funktionen jedoch schwierig. Bei Strukturen ist das Hinzufügen von Funktionen einfach, das Hinzufügen von Typen jedoch schwierig.
  • Datenstrukturen führen zu einer Neukompilierung und Neuverteilung des aufrufenden Codes. Klassen isolieren den aufrufenden Code und müssen ihn nicht erneut kompilieren und bereitstellen.

Ja, das ist richtig. Dies sollte jeder Designer und Software-Architekt berücksichtigen.

Source: https://habr.com/ru/post/de477712/


All Articles