Antes de la OOP, ¿se dejaban públicos los miembros de la estructura de datos?

44

Cuando se implementa una estructura de datos (por ejemplo, una cola) utilizando un lenguaje OOP, algunos miembros de la estructura de datos deben ser privados (por ejemplo, el número de elementos en la cola).

Una cola también se puede implementar en un lenguaje de procedimiento utilizando ay structun conjunto de funciones que operan en el struct. Sin embargo, en un lenguaje de procedimiento no puede hacer que los miembros de un structprivado. ¿Se dejaron públicos los miembros de una estructura de datos implementados en un lenguaje de procedimiento, o hubo algún truco para hacerlos privados?

Christopher
fuente
75
"algunos miembros de la estructura de datos deben ser privados" Hay una gran diferencia entre "probablemente debería ser" y "necesita ser". Dame un lenguaje OO y te garantizo que puedo hacer una cola que funcione perfectamente bien incluso con todos sus miembros y métodos públicos, siempre y cuando no abuses de toda esa libertad.
8bittree
48
Para darle un giro diferente a lo que dijo @ 8bittree, tener todo público está bien si las personas que usan su código son lo suficientemente disciplinadas como para adherirse a la interfaz que ha establecido. La construcción del miembro privado surgió debido a personas que no podían mantener sus narices fuera de donde no pertenecen.
Blrfl
20
¿Quiso decir "antes de que la encapsulación se hiciera popular"? La encapsulación era bastante popular antes de que los lenguajes OO se hicieran populares.
Frank Hileman
66
@FrankHileman Creo que ese es realmente el núcleo de la pregunta: OP quiere saber si la encapsulación existió en lenguajes de procedimiento, antes de Simula / Smalltalk / C ++
dcorking
18
Lo siento de antemano si esto parece ser condescendiente, no quiero decir que sea así. Necesitas aprender algunos otros idiomas. Los lenguajes de programación no son para que las máquinas funcionen, son para que los programadores piensen en ellos . Forman necesariamente la forma en que piensas. Simplemente no tendría esta pregunta si hubiera pasado un tiempo significativo trabajando con JavaScript / Python / Ocaml / Clojure, incluso si hiciera Java todo el día en su trabajo diario. Aparte de un proyecto de código abierto de C ++ en el que trabajo (que en su mayoría es C de todos modos) Realmente no he usado un lenguaje con modificadores de acceso desde la universidad, y no me los he perdido.
Jared Smith

Respuestas:

139

OOP no inventó la encapsulación y no es sinónimo de encapsulación. Muchos lenguajes OOP no tienen modificadores de acceso de estilo C ++ / Java. Muchos lenguajes que no son OOP tienen varias técnicas disponibles para ofrecer encapsulación.

Un enfoque clásico para la encapsulación es el cierre , como se usa en la programación funcional . Esto es significativamente más antiguo que OOP pero es de alguna manera equivalente. Por ejemplo, en JavaScript podríamos crear un objeto como este:

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

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

El plus2objeto anterior no tiene ningún miembro que permita el acceso directo x, está completamente encapsulado. El add()método es un cierre sobre la xvariable.

El lenguaje C admite algunos tipos de encapsulación a través de su mecanismo de archivo de encabezado , particularmente la técnica de puntero opaco . En C, es posible declarar un nombre de estructura sin definir sus miembros. En ese momento, no se puede usar ninguna variable del tipo de esa estructura, pero podemos usar punteros a esa estructura libremente (porque el tamaño de un puntero de estructura se conoce en tiempo de compilación). Por ejemplo, considere este archivo de encabezado:

#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

Ahora podemos escribir código que use esta interfaz Adder, sin tener acceso a sus campos, por ejemplo:

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

Y aquí estarían los detalles de implementación totalmente encapsulados:

#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;
}

También existe la clase de lenguajes de programación modulares , que se centra en las interfaces de nivel de módulo. La familia de idiomas ML incl. OCaml incluye un enfoque interesante para los módulos llamados functores . OOP eclipsó y moduló en gran medida la programación modular, sin embargo, muchas de las supuestas ventajas de OOP tienen más que ver con la modularidad que con la orientación a objetos.

También se observa que las clases en lenguajes OOP como C ++ o Java a menudo no se usan para objetos (en el sentido de entidades que resuelven operaciones mediante un enlace tardío / despacho dinámico), sino simplemente para tipos de datos abstractos (donde definimos una interfaz pública que oculta detalles de implementación interna). El documento Sobre la comprensión de la abstracción de datos, revisado (Cook, 2009) analiza esta diferencia con más detalle.

Pero sí, muchos idiomas no tienen ningún mecanismo de encapsulación. En estos idiomas, los miembros de la estructura se dejan públicos. A lo sumo, habría una convención de nomenclatura para desalentar el uso. Por ejemplo, creo que Pascal no tenía un mecanismo de encapsulación útil.

amon
fuente
11
¿Ves el error en Adder self = malloc(sizeof(Adder));? Hay una razón por la que se escriben punteros y sizeof(TYPE)generalmente está mal visto.
Deduplicador
10
No puedes simplemente escribir sizeof(*Adder), porque *Adderno es un tipo, así como *int *tampoco es un tipo. La expresión T t = malloc(sizeof *t)es idiomática y correcta. Mira mi edición.
wchargin
44
Pascal tenía variables de unidad que no podían verse desde fuera de esa unidad. Efectivamente, las variables unitarias eran equivalentes a las private staticvariables en Java. Del mismo modo, para C podría usar punteros opacos para pasar datos en Pascal sin declarar de qué se trataba. El MacOS clásico utilizaba muchos punteros opacos, ya que las partes públicas y privadas de un registro (estructura de datos) podían pasarse juntas. Recuerdo que Window Manager hizo mucho de esto, ya que partes del Registro de Windows eran públicas, pero también se incluyó cierta información interna.
Michael Shopsin
66
Quizás un mejor ejemplo que Pascal es Python, que admite la orientación a objetos pero no la encapsulación, recurriendo a convenciones de nomenclatura como _private_membery output_property_, o técnicas más avanzadas para hacer objetos imputables.
Mephy
11
Hay una tendencia molesta en la literatura OOD a presentar cada principio de diseño como un principio de diseño OO . La literatura de OOD (no académica) tiende a pintar una imagen de una "edad oscura" donde todos estaban haciendo todo mal, y luego los practicantes de OOP traen la luz. Por lo que puedo decir, esto se deriva principalmente de la ignorancia. Por ejemplo, por lo que puedo decir, Bob Martin le dio a la programación funcional un aspecto serio hace solo unos años.
Derek Elkins el
31

Primero, ser procesal versus orientado a objetos no tiene nada que ver con lo público versus lo privado. Muchos lenguajes orientados a objetos no tienen noción de control de acceso.

En segundo lugar, en "C", que la mayoría de las personas llamaría de procedimiento, y no orientado a objetos, hay muchos trucos que puedes usar para hacer que las cosas sean privadas de manera efectiva. Una muy común es utilizar punteros opacos (por ejemplo, nulo *). O bien, puede reenviar la declaración de un objeto y no definirlo en un archivo de encabezado.

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) { ... }

¡Mira el SDK de Windows! Utiliza HANDLE y UINT_PTR, y cosas así para ser manejadores genéricos de la memoria utilizada en las API, lo que hace que las implementaciones sean privadas.

Lewis Pringle
fuente
1
Mi muestra demostró un mejor enfoque (C), utilizando estructuras declaradas hacia adelante. para usar el enfoque void *, usaría typedefs: en el archivo .h di typedef void * queue, y luego en todas partes teníamos struct queue solo di queue; Luego, en el archivo .c, cambie el nombre de struct queue a struct queueImpl, y cualquier argumento se convertirá en cola (isntead de struct queue *) y la primera línea de código para cada función se convertirá en struct queueImpl * qi = (struct queueImpl *) q
Lewis Pringle
77
Hmm Lo hace privado porque no puede acceder (leer o escribir) a ningún campo de la 'cola' desde ningún lugar además de su implementación (archivo foo.c). ¿Qué más quisiste decir con privado? Por cierto, eso es cierto para AMBOS el typedef void * apporach y el (mejor) enfoque de declarar adelante estructura
Lewis Pringle
55
Debo confesar que han pasado casi 40 años desde que leí el libro sobre smalltalk-80, pero no recuerdo ninguna noción de miembros de datos públicos o privados. Creo que CLOS tampoco tenía esa noción. Objeto Pascal no tenía tal noción. Recuerdo que Simula sí (probablemente de dónde sacó Stroustrup la idea), y la mayoría de los lenguajes OO desde C ++ lo tienen. De todos modos, estamos de acuerdo en que la encapsulación y los datos privados son buenas ideas. Incluso el interrogador original fue claro en ese punto. Solo preguntaba: ¿cómo hicieron los viejos encapsulamientos en lenguajes anteriores a C ++?
Lewis Pringle
55
@LewisPringle no hay mención de miembros de datos públicos en Smalltalk-80 porque todas las "variables de instancia" (miembros de datos) son privadas, a menos que use la reflexión. AFAIU Smalltalkers escribe un descriptor de acceso para cada variable que quieren hacer público.
dcorking
44
@LewisPringle por el contrario, todos los "métodos" Smalltalk (miembros de función) son públicos (hay convenciones torpes para marcarlas privada)
dcorking
13

"Tipos de datos opacos" era un concepto bien conocido cuando obtuve mi título en informática hace 30 años. No cubrimos OOP ya que no era de uso común en ese momento y la "programación funcional" se consideraba más correcta.

Modula-2 tenía soporte directo para ellos, ver https://www.modula2.org/reference/modules.php .

Lewis Pringle ya ha explicado cómo se puede utilizar la declaración progresiva de una estructura en C. A diferencia del Módulo 2, se tenía que proporcionar una función de fábrica para crear el objeto. ( Los métodos virtuales también fueron fáciles de implementar en C al hacer que el primer miembro de una estructura sea un puntero a otra estructura que contenía punteros de función a los métodos).

A menudo también se usó la convención. Por ejemplo, no se debe acceder a ningún campo que comience con "_" fuera del archivo que poseía los datos. Esto se hizo cumplir fácilmente mediante la creación de herramientas de verificación personalizadas.

Todos los proyectos a gran escala en los que he trabajado (antes de pasar a C ++ y luego a C #) tenían un sistema para evitar que el código incorrecto acceda a los datos "privados". Fue un poco menos estandarizado de lo que es ahora.

Ian
fuente
9

Tenga en cuenta que hay muchos idiomas OO sin una capacidad incorporada para marcar miembros como privados. Esto puede hacerse por convención, sin necesidad de que el compilador imponga la privacidad. Por ejemplo, las personas a menudo prefijarán variables privadas con un guión bajo.

Existen técnicas para dificultar el acceso a variables "privadas", siendo la más común el lenguaje PIMPL . Esto coloca sus variables privadas en una estructura separada, con solo un puntero asignado en sus archivos de encabezado públicos. Esto significa una desreferencia adicional y un reparto para obtener variables privadas, algo así como ((private_impl)(obj->private))->actual_value, lo que se vuelve molesto, por lo que en la práctica rara vez se usa.

Karl Bielefeldt
fuente
4

Las estructuras de datos no tenían "miembros", solo campos de datos (suponiendo que fuera un tipo de registro). La visibilidad generalmente se estableció para todo el tipo. Sin embargo, eso puede no ser tan limitante como cree, porque las funciones no formaban parte del registro.

Regresemos y obtengamos un poco de historia aquí ...

El paradigma de programación dominante antes de OOP se llamaba programación estructurada . El objetivo principal inicial de esto era evitar el uso de declaraciones de salto no estructuradas ("goto" s). Este es un paradigma orientado al flujo de control (mientras que OOP está más orientado a los datos), pero todavía era una extensión natural para intentar mantener los datos estructurados lógicamente al igual que el código.

Otro resultado de la programación estructurada fue la ocultación de información , la idea de que las implementaciones de la estructura del código (que probablemente cambie con bastante frecuencia) deberían mantenerse separadas de la interfaz (que idealmente no cambiará tanto). Ahora es un dogma, pero en los viejos tiempos, muchas personas realmente consideraban mejor que cada desarrollador conociera los detalles de todo el sistema, por lo que en un momento fue una idea controvertida. La edición original de The Mythical Man Month de Brook en realidad argumentaba en contra de la ocultación de información.

Los lenguajes de programación posteriores diseñados explícitamente para ser buenos lenguajes de programación estructurados (por ejemplo, Modula-2 y Ada) generalmente incluían la ocultación de información como un concepto fundamental, construido alrededor de algún tipo de concepto de una instalación cohesiva de funciones (y cualquier tipo, constantes y objetos que puedan necesitar). En Modula-2 estos se llamaron "Módulos", en Ada "Paquetes". Muchos lenguajes OOP modernos llaman al mismo concepto "espacios de nombres". Estos espacios de nombres fueron la base organizativa del desarrollo en estos lenguajes, y para la mayoría de los propósitos podrían usarse de manera similar a las clases OOP (por supuesto, sin soporte real para la herencia).

Entonces, en Modula-2 y Ada (83) podría declarar cualquier rutina, tipo, constante u objeto en un espacio de nombres privado o público, pero si tenía un tipo de registro, no había forma (fácil) de declarar algunos campos de registro públicos y otros privados. O todo su registro es público, o no lo es.

TED
fuente
Pasé bastante tiempo trabajando en Ada. La ocultación selectiva (de parte de un tipo de datos) era algo que hacíamos todo el tiempo; en el paquete contenedor, definiría el tipo en sí como privado o privado limitado; la interfaz del paquete expondría funciones / procedimientos públicos para obtener y / o establecer campos internos. Por supuesto, esas rutinas necesitarían tomar un parámetro del tipo privado. No lo hice entonces y ahora no considero esto difícil.
David
Además, AFAIK la mayoría de los idiomas OO funcionan de la misma manera, es decir, myWidget.getFoo () se implementa realmente como getFoo (myWidget). La object.method()invocación es solo azúcar sintáctico. En mi humilde opinión, vea el Principio de acceso / referencia uniforme de Meyer, pero aún así solo el azúcar sintáctico.
David
@David: Ese fue el argumento de la comunidad Ada durante años durante la era Ada 95. Creo que finalmente cedieron y probaron su propio argumento al permitirlo object.method()como una forma alternativa method(object, ...) para las personas que simplemente no podían dar el salto conceptual.
TED
0

En C, ya podría pasar punteros a tipos declarados pero indefinidos como otros han dicho, en efecto restringiendo el acceso a todos los campos.

También puede tener funciones privadas y públicas módulo a módulo. Las funciones declaradas estáticas en el archivo fuente no son visibles para el exterior, incluso si intenta adivinar su nombre. Del mismo modo, puede tener variables globales estáticas a nivel de archivo, lo que generalmente es una mala práctica, pero permite el aislamiento por módulos.

Probablemente sea importante destacar que la restricción de acceso como una convención bien estandarizada en lugar de una construcción aplicada por el lenguaje funciona bien (ver Python). Además de eso, restringir el acceso a los campos de objetos solo protegerá al programador cuando sea necesario cambiar el valor de los datos dentro de un objeto después de la creación. Que ya es un olor a código. Podría decirse que la constpalabra clave de C y, en particular, de C ++ para métodos y argumentos de función es una ayuda mucho mayor para el programador que la bastante pobre de Java final.

Kafein
fuente
La única característica que C tenía que era específicamente para ocultar información era staticdatos y operaciones globales (lo que significaba que no se presentaron al enlazador para su uso desde otras compilaciones). Puede argumentar plausiblemente que cualquier soporte que C tenía para buenas prácticas de diseño de software, aparte de eso, fue prácticamente un truco, y no forma parte del diseño original del lenguaje en 1972.
TED
0

Si su definición de Público es la capacidad de acceder a la implementación y a los datos / propiedades a través de su propio código en cualquier momento, la respuesta es simplemente: . Sin embargo, fue abstraído por diversos medios, dependiendo del idioma.

Espero que esto haya respondido sucintamente a su pregunta.

RobMac
fuente
-1

Aquí hay un contraejemplo muy simple: en Java, interfaces define objetos, pero classes no. A classdefine un tipo de datos abstractos, no un objeto.

Ergo, cada vez que usa privateun classen Java, tiene un ejemplo de una estructura de datos con miembros privados que no está orientada a objetos.

Jörg W Mittag
fuente
77
Por supuesto, esta respuesta es técnicamente correcta, pero es totalmente incomprensible para cualquiera que no sepa cómo son los ADT y cómo son diferentes de los objetos.
amon
1
Aprendí algo de esta respuesta.
littleO
3
Las interfaces no "definen" objetos; que especifican los contratos para operaciones / comportamientos que los objetos pueden hacer o llevar a cabo. Al igual que la herencia es generalmente descrita por un es una relación y la composición por un tiene una relación, las interfaces se describen generalmente por relaciones de poder hacer .
code_dredd