Wurden Mitglieder der Datenstruktur vor OOP öffentlich gelassen?

44

Wenn eine Datenstruktur (z. B. eine Warteschlange) in einer OOP-Sprache implementiert wird, müssen einige Mitglieder der Datenstruktur privat sein (z. B. die Anzahl der Elemente in der Warteschlange).

Eine Warteschlange kann auch in einer prozeduralen Sprache implementiert werden, indem a structund eine Reihe von Funktionen verwendet werden, die auf dem Server ausgeführt werden struct. In einer prozeduralen Sprache können Sie die Mitglieder jedoch nicht zu structPrivaten machen. Wurden die Mitglieder einer Datenstruktur, die in einer prozeduralen Sprache implementiert wurde, öffentlich gelassen, oder gab es einen Trick, um sie privat zu machen?

Christopher
quelle
75
"Einige Mitglieder der Datenstruktur müssen privat sein" Es gibt einen großen Unterschied zwischen "sollte" und "muss". Geben Sie mir eine OO-Sprache und ich garantiere, dass ich eine Warteschlange erstellen kann, die auch dann einwandfrei funktioniert, wenn alle Mitglieder und Methoden öffentlich sind, solange Sie diese Freiheit nicht missbrauchen.
8bittree,
48
Um das, was @ 8bittree gesagt hat, anders zu interpretieren, ist es in Ordnung, alles öffentlich zu haben, wenn die Leute, die Ihren Code verwenden, diszipliniert genug sind, um sich an die von Ihnen vorgegebene Schnittstelle zu halten. Das private Mitgliedskonstrukt entstand aufgrund von Leuten, die ihre Nasen nicht dorthin halten konnten, wo sie nicht hingehören.
Blrfl
20
Meinten Sie "bevor die Kapselung populär wurde"? Die Verkapselung war ziemlich populär, bevor die OO-Sprachen populär wurden.
Frank Hileman
6
@FrankHileman Ich denke, das ist eigentlich der Kern der Frage: OP möchte wissen, ob die Kapselung in prozeduralen Sprachen existierte, bevor Simula / Smalltalk / C ++
entkorken
18
Es tut mir im Voraus leid, wenn dies herablassend rüberkommt, ich meine es nicht so. Sie müssen einige andere Sprachen lernen. Programmiersprachen sind nicht für Maschinen gedacht , sondern für Programmierer . Sie prägen unbedingt die Art und Weise Sie denken. Sie hätten diese Frage einfach nicht, wenn Sie viel Zeit mit JavaScript / Python / Ocaml / Clojure verbracht hätten, selbst wenn Sie den ganzen Tag Java bei Ihrer Arbeit getan hätten. Abgesehen von einem C ++ - Open-Source-Projekt, an dem ich arbeite (das ist sowieso meistens C), habe ich seit dem College keine Sprache mit Zugriffsmodifikatoren mehr verwendet, und ich habe sie auch nicht vermisst.
Jared Smith

Antworten:

139

OOP hat die Kapselung nicht erfunden und ist nicht gleichbedeutend mit der Kapselung. Viele OOP-Sprachen verfügen nicht über Zugriffsmodifikatoren im C ++ / Java-Stil. Viele Nicht-OOP-Sprachen verfügen über verschiedene Techniken, um die Kapselung zu ermöglichen.

Ein klassischer Ansatz für die Kapselung sind Verschlüsse , wie sie in der funktionalen Programmierung verwendet werden . Dies ist deutlich älter als OOP, aber in gewisser Weise gleichwertig. Beispielsweise können wir in JavaScript ein Objekt wie das folgende erstellen:

function Adder(x) {
  this.add = function add(y) {
    return x + y;
  }
}

var plus2 = new Adder(2);
plus2.add(7);  //=> 9

Das obige plus2Objekt hat kein Mitglied, auf das direkt zugegriffen werden kann x- es ist vollständig gekapselt. Die add()Methode ist ein Abschluss über die xVariable.

Die C- Sprache unterstützt einige Arten der Kapselung durch ihren Header- Dateimechanismus, insbesondere die opake Zeigertechnik . In C ist es möglich, einen Strukturnamen zu deklarieren, ohne seine Mitglieder zu definieren. Zu diesem Zeitpunkt kann keine Variable des Typs dieser Struktur verwendet werden, aber wir können Zeiger auf diese Struktur frei verwenden (da die Größe eines Strukturzeigers zur Kompilierungszeit bekannt ist). Betrachten Sie zum Beispiel diese Header-Datei:

#ifndef ADDER_H
#define ADDER_H

typedef struct AdderImpl *Adder;

Adder Adder_new(int x);
void Adder_free(Adder self);
int Adder_add(Adder self, int y);

#endif

Wir können jetzt Code schreiben, der diese Adder-Schnittstelle verwendet, ohne Zugriff auf seine Felder zu haben, zB:

Adder plus2 = Adder_new(2);
if (!plus2) abort();
printf("%d\n", Adder_add(plus2, 7));  /* => 9 */
Adder_free(plus2);

Und hier wären die vollständig gekapselten Implementierungsdetails:

#include "adder.h"

struct AdderImpl { int x; };

Adder Adder_new(int x) {
  Adder self = malloc(sizeof *self);
  if (!self) return NULL;
  self->x = x;
  return self;
}

void Adder_free(Adder self) {
  free(self);
}

int Adder_add(Adder self, int y) {
  return self->x + y;
}

Es gibt auch die Klasse der modularen Programmiersprachen , die sich auf Schnittstellen auf Modulebene konzentriert. Die ML-Sprachfamilie inkl. OCaml enthält einen interessanten Ansatz für Module, die als Funktoren bezeichnet werden . OOP hat die modulare Programmierung überschattet und weitestgehend unterbewertet, doch bei vielen angeblichen Vorteilen von OOP geht es mehr um Modularität als um Objektorientierung.

Es gibt auch die Beobachtung, dass Klassen in OOP-Sprachen wie C ++ oder Java oft nicht für Objekte verwendet werden (im Sinne von Entitäten, die Operationen durch späte Bindung / dynamischen Versand auflösen), sondern nur für abstrakte Datentypen (wobei wir eine öffentliche Schnittstelle definieren, die verborgen bleibt) Details zur internen Implementierung). In dem Aufsatz Über das Verständnis der Datenabstraktion, Revisited (Cook, 2009) wird dieser Unterschied ausführlicher erörtert.

Aber ja, viele Sprachen haben überhaupt keinen Verkapselungsmechanismus. In diesen Sprachen werden Strukturmitglieder öffentlich gelassen. Allenfalls würde eine Namenskonvention von der Verwendung abhalten. Ich glaube, Pascal hatte keinen nützlichen Einkapselungsmechanismus.

amon
quelle
11
Sehen Sie den Fehler in Adder self = malloc(sizeof(Adder));? Es gibt einen Grund, warum man Zeiger tippt und sizeof(TYPE)im Allgemeinen verpönt ist.
Deduplizierer
10
Sie können nicht einfach schreiben sizeof(*Adder), weil *Adderes kein Typ ist, genauso wie *int *es kein Typ ist. Der Ausdruck T t = malloc(sizeof *t)ist sowohl idiomatisch als auch korrekt. Siehe meine Bearbeitung.
Wchargin
4
Pascal hatte Einheitenvariablen, die von außerhalb dieser Einheit nicht sichtbar waren. Tatsächlich entsprachen die Einheitenvariablen den private staticVariablen in Java. Ähnlich wie bei C können Sie auch undurchsichtige Zeiger verwenden, um Daten in Pascal zu übergeben, ohne zu deklarieren, um was es sich handelt. Das klassische MacOS verwendete viele undurchsichtige Zeiger, da öffentliche und private Teile eines Datensatzes (Datenstruktur) möglicherweise zusammen übergeben werden. Ich erinnere mich, dass Window Manager viel davon getan hat, da Teile des Fensterdatensatzes öffentlich waren, aber auch einige interne Informationen enthalten waren.
Michael Shopsin
6
Ein vielleicht besseres Beispiel als Pascal ist Python, das Objektorientierung, aber keine Kapselung unterstützt und auf Namenskonventionen wie _private_memberund output_property_oder fortgeschrittenere Techniken zum Erstellen von unveränderlichen Objekten zurückgreift.
Mephy
11
In der OOD-Literatur gibt es eine ärgerliche Tendenz, jedes Konstruktionsprinzip als OO- Konstruktionsprinzip darzustellen. Die (nicht-akademische) OOD-Literatur tendiert dazu, ein Bild eines "dunklen Zeitalters" zu malen, in dem jeder alles falsch gemacht hat, und dann bringen die OOP-Praktizierenden das Licht. Soweit ich das beurteilen kann, beruht dies größtenteils auf Unwissenheit. Zum Beispiel hat Bob Martin, soweit ich das beurteilen kann, der funktionalen Programmierung erst vor einigen Jahren einen ernsten Ausdruck verliehen.
Derek Elkins
31

Erstens hat prozedural versus objektorientiert nichts mit öffentlich oder privat zu tun. Viele objektorientierte Sprachen kennen keine Zugriffskontrolle.

Zweitens gibt es in "C" - was die meisten Leute als prozedural und nicht objektorientiert bezeichnen würden - viele Tricks, mit denen Sie Dinge effektiv privat machen können. Sehr häufig werden undurchsichtige (z. B. leere *) Zeiger verwendet. Oder - Sie können ein Objekt weiterleiten, ohne es in einer Header-Datei zu definieren.

foo.h:

struct queue;
struct queue* makeQueue();
void add2Queue(struct queue* q, int value);
...

foo.c:

struct queue {
    int* head;
    int* head;
};
struct queue* makeQueue() { .... }
void add2Queue(struct queue* q, int value) { ... }

Schau dir das Windows SDK an! Es verwendet HANDLE und UINT_PTR und solche Dinge sind generische Handles für den in APIs verwendeten Speicher - wodurch die Implementierungen effektiv privat werden.

Lewis Pringle
quelle
1
Meine Stichprobe zeigte einen besseren (C) Ansatz - unter Verwendung von vorwärts deklarierten Strukturen. Um den void * -Ansatz zu verwenden, würde ich typedefs verwenden: In der .h-Datei sagen Sie typedef void * queue, und überall, wo wir struct queue hatten, sagen Sie einfach queue; Benennen Sie dann in der Datei .c die Strukturwarteschlange in struct queueImpl um, und alle Argumente werden zur Warteschlange (anstelle der Strukturwarteschlange *). Die erste Codezeile für jede dieser Funktionen wird zu struct queueImpl * qi = (struct queueImpl *) q
Lewis Pringle
7
Hmm. Es macht es privat, weil Sie nicht von jedem Ort aus auf Felder der 'Warteschlange' zugreifen (lesen oder schreiben) können, außer auf die Implementierung (Datei foo.c). Was meintest du sonst noch mit privat? Übrigens - das gilt sowohl für den typedef void * -Ansatz als auch für den (besseren) forward declare struct approach
Lewis Pringle
5
Ich muss zugeben, dass es fast 40 Jahre her ist, seit ich das Buch über Smalltalk-80 gelesen habe, aber ich erinnere mich nicht an öffentliche oder private Datenmitglieder. Ich denke, CLOS hatte auch keine solche Vorstellung. Object Pascal hatte keine solche Vorstellung. Ich erinnere mich, dass Simula es getan hat (wahrscheinlich wo Stroustrup auf die Idee gekommen ist), und die meisten OO-Sprachen seit C ++ haben es. Wie auch immer - wir sind uns einig, dass Verkapselung und private Daten gute Ideen sind. Sogar der ursprüngliche Fragesteller war in diesem Punkt klar. Er fragte nur: Wie haben Oldies die Kapselung in Sprachen vor C ++ durchgeführt?
Lewis Pringle
5
@LewisPringle In Smalltalk-80 werden keine öffentlichen Datenelemente erwähnt, da alle "Instanzvariablen" (Datenelemente) privat sind, es sei denn, Sie verwenden Reflektion. AFAIU Smalltalker schreiben einen Accessor für jede Variable, die sie veröffentlichen möchten.
Dcorking
4
@LewisPringle im Gegensatz dazu sind alle Smalltalk "Methoden" (Funktionsmitglieder) öffentlich (es gibt ungeschickte Konventionen, um sie als privat zu
kennzeichnen
13

"Undurchsichtige Datentypen" war ein bekanntes Konzept, als ich vor 30 Jahren meinen Abschluss in Informatik machte. Wir haben OOP nicht behandelt, da es zu diesem Zeitpunkt nicht gebräuchlich war und "funktionale Programmierung" als korrekter angesehen wurde.

Modula-2 hatte direkte Unterstützung für sie, siehe https://www.modula2.org/reference/modules.php .

Lewis Pringle hat bereits erklärt, wie die Forward-Deklaration einer Struktur in C verwendet werden kann. Im Gegensatz zu Modul 2 musste zum Erstellen des Objekts eine Factory-Funktion bereitgestellt werden. ( Virtuelle Methoden waren auch einfach in C zu implementieren, indem das erste Element einer Struktur ein Zeiger auf eine andere Struktur war, die Funktionszeiger auf die Methoden enthielt.)

Oft wurde auch Konvention verwendet. Beispielsweise sollte auf Felder, die mit "_" beginnen, nicht außerhalb der Datei zugegriffen werden, deren Eigentümer die Daten sind. Dies wurde leicht durch die Erstellung von benutzerdefinierten Überprüfungswerkzeugen durchgesetzt.

Jedes große Projekt, an dem ich gearbeitet habe (bevor ich auf C ++ umgestiegen bin, dann auf C #), hatte ein System, um zu verhindern, dass auf "private" Daten mit dem falschen Code zugegriffen wird. Es war nur ein bisschen weniger standardisiert als jetzt.

Ian
quelle
9

Beachten Sie, dass es viele OO-Sprachen gibt, in denen Mitglieder nicht als privat markiert werden können. Dies kann durch Konvention erfolgen, ohne dass der Compiler die Privatsphäre durchsetzen muss. Beispielsweise wird privaten Variablen häufig ein Unterstrich vorangestellt.

Es gibt Techniken, die den Zugriff auf "private" Variablen erschweren . Am häufigsten wird die PIMPL-Sprache verwendet . Dies versetzt Ihre privaten Variablen in eine separate Struktur, wobei nur ein Zeiger in Ihren öffentlichen Header-Dateien zugewiesen wird. Dies bedeutet eine zusätzliche Dereferenzierung und eine Besetzung, um etwa private Variablen zu erhalten, was ((private_impl)(obj->private))->actual_valueärgerlich wird, weshalb in der Praxis selten Gebrauch gemacht wird.

Karl Bielefeldt
quelle
4

Datenstrukturen hatten keine "Mitglieder", nur Datenfelder (vorausgesetzt, es war ein Datensatztyp). Die Sichtbarkeit wurde normalerweise für den gesamten Typ festgelegt. Dies ist jedoch möglicherweise nicht so einschränkend, wie Sie denken, da die Funktionen nicht Teil des Datensatzes waren.

Kommen wir zurück und werfen einen Blick auf die Geschichte ...

Das vorherrschende Programmierparadigma vor OOP wurde als strukturierte Programmierung bezeichnet . Das ursprüngliche Hauptziel war es, die Verwendung von unstrukturierten Sprunganweisungen ("goto" s) zu vermeiden. Dies ist ein Kontrollfluss-orientiertes Paradigma (während OOP mehr datenorientiert ist), aber es war immer noch eine logische Erweiterung davon, um zu versuchen, Daten logisch strukturiert zu halten, genau wie der Code.

Ein weiterer Ausweg aus der strukturierten Programmierung war das Verbergen von Informationen , die Idee, dass die Implementierungen der Codestruktur (die sich wahrscheinlich ziemlich oft ändern) von der Schnittstelle getrennt werden sollten (die sich im Idealfall nicht annähernd so stark ändert). Heutzutage ist es ein Dogma, aber in früheren Zeiten hielten es viele Leute für besser, wenn jeder Entwickler die Details des gesamten Systems kennenlernte. Dies war also zu einer Zeit eine umstrittene Idee. Die Originalausgabe von Brooks The Mythical Man Month sprach sich tatsächlich gegen das Verstecken von Informationen aus.

Spätere Programmiersprachen, die explizit als gute strukturierte Programmiersprachen konzipiert wurden (z. B. Modula-2 und Ada), enthielten im Allgemeinen Informationen, die sich als grundlegendes Konzept verbargen und auf eine Art Konzept einer zusammenhängenden Funktionseinheit (und beliebiger Typen, Konstanten und Objekte, die sie möglicherweise benötigen). In Modula-2 wurden diese "Module" genannt, in Ada "Packages". Viele moderne OOP-Sprachen nennen das gleiche Konzept "Namespaces". Diese Namespaces bildeten die organisatorische Grundlage für die Entwicklung in diesen Sprachen und konnten für die meisten Zwecke ähnlich wie OOP-Klassen verwendet werden (natürlich ohne echte Unterstützung für die Vererbung).

So in Modula-2 und Ada (83) können Sie jede Routine, Typen, konstant erklären, oder ein Objekt in einem Namespace privat oder öffentlich, aber wenn Sie einen Datensatztyp haben, gibt es keinen (leicht) Weg , um etwas Rekord zu erklären Felder Öffentlichkeit und andere privat. Entweder ist Ihre gesamte Aufzeichnung öffentlich oder nicht.

TED
quelle
Ich habe ziemlich viel Zeit in Ada verbracht. Das selektive Ausblenden (eines Teils eines Datentyps) haben wir die ganze Zeit durchgeführt. Im übergeordneten Paket definieren Sie den Typ selbst als privat oder als beschränkt privat. Die Paketschnittstelle würde öffentliche Funktionen / Prozeduren verfügbar machen, um interne Felder abzurufen und / oder festzulegen. Diese Routinen müssten natürlich einen Parameter des privaten Typs annehmen. Das habe ich damals und heute nicht für schwierig gehalten.
David
Auch AFAIK die meisten OO-Sprachen arbeiten auf die gleiche Weise unter der Haube, dh myWidget.getFoo () ist wirklich als getFoo (myWidget) implementiert. Der object.method()Aufruf ist nur syntaktischer Zucker. Wichtige IMHO - siehe Meyers Prinzip des einheitlichen Zugangs / Referenz - aber immer noch nur syntaktischer Zucker.
David
@David - Das war jahrelang das Argument der Ada-Community während der Ada 95-Ära. Ich glaube, sie haben endlich nachgegeben und ihr eigenes Argument unter Beweis gestellt, indem sie Menschen, die den konzeptionellen Sprung einfach nicht schaffen konnten, object.method()eine alternative Form method(object, ...) zugestanden haben.
TED
0

In C könnten Sie bereits Zeiger auf deklarierte, aber undefinierte Typen übergeben, wie andere bereits gesagt haben, wodurch der Zugriff auf alle Felder eingeschränkt wird.

Sie können auch private und öffentliche Funktionen von Modul zu Modul ausführen. In der Quelldatei als statisch deklarierte Funktionen sind von außen nicht sichtbar, auch wenn Sie versuchen, ihren Namen zu erraten. In ähnlicher Weise können Sie globale Variablen auf statischer Dateiebene verwenden, was im Allgemeinen keine gute Praxis ist, aber die Isolation auf Modulbasis ermöglicht.

Es ist wahrscheinlich wichtig zu betonen, dass Zugriffsbeschränkungen als eine gut standardisierte Konvention und nicht als ein durch Sprache erzwungenes Konstrukt gut funktionieren (siehe Python). Darüber hinaus schützt die Einschränkung des Zugriffs auf Objektfelder den Programmierer immer dann, wenn der Wert der Daten in einem Objekt nach der Erstellung geändert werden muss. Welches ist schon ein Code-Geruch. Das constSchlüsselwort von C und insbesondere von C ++ für Methoden und Funktionsargumente ist für den Programmierer vermutlich eine weitaus größere Hilfe als das eher schlechte von Java final.

Kafein
quelle
Das einzige Feature, das C speziell zum Ausblenden von Informationen hatte, waren staticglobale Daten und Operationen (was bedeutete, dass sie dem Linker nicht zur Verwendung durch andere Zusammenstellungen präsentiert wurden). Sie können plausibel argumentieren, dass jede Unterstützung, die C für gute Software-Design-Praktiken hatte, abgesehen davon ein Hack war und nicht Teil des ursprünglichen Designs der Sprache aus dem Jahr 1972.
TED
0

Wenn Ihre Definition von Öffentlich die Möglichkeit ist, jederzeit über Ihren eigenen Code auf Implementierung und Daten / Eigenschaften zuzugreifen, lautet die Antwort einfach: Ja . Sie wurde jedoch - je nach Sprache - auf unterschiedliche Weise abstrahiert.

Ich hoffe, das hat Ihre Frage kurz und bündig beantwortet.

RobMac
quelle
-1

Hier ist ein sehr einfaches Gegenbeispiel: In Java interfacedefinieren s Objekte, aber classes nicht. A classdefiniert einen abstrakten Datentyp, kein Objekt.

Ergo, zu jeder Zeit Sie verwenden privatein einem classin Java, haben Sie ein Beispiel für eine Datenstruktur mit privaten Mitgliedern , die nicht objektorientiert ist.

Jörg W. Mittag
quelle
7
Diese Antwort ist natürlich technisch korrekt, aber für jeden, der noch nicht weiß, wie ADTs sind und wie sie sich von Objekten unterscheiden, völlig unverständlich.
Amon
1
Aus dieser Antwort habe ich etwas gelernt.
LittleO
3
Schnittstellen "definieren" keine Objekte. sie geben Aufträge für Operationen / Verhaltensweisen , dass Objekte tun können oder ausführen. So wie Vererbung im Allgemeinen durch eine Beziehung beschrieben wird und Zusammensetzung durch eine Beziehung, werden Schnittstellen im Allgemeinen durch Can-Do- Beziehungen beschrieben.
Code_dredd