До ООП члены структуры данных оставались открытыми?


44

Когда структура данных (например, очередь) реализуется с использованием языка ООП, некоторые члены структуры данных должны быть частными (например, количество элементов в очереди).

Очередь также может быть реализована на процедурном языке с использованием structфункций и набора функций, которые работают на struct. Тем не менее, на процедурном языке вы не можете сделать членов structчастного. Были ли члены структуры данных, реализованные на процедурном языке, оставлены публичными, или был какой-то прием, чтобы сделать их приватными?


75
«Некоторые члены структуры данных должны быть частными» Существует большая разница между «вероятно, должно быть» и «должно быть». Дайте мне язык OO, и я гарантирую, что могу создать очередь, которая прекрасно работает, даже если все ее члены и методы являются публичными, если вы не злоупотребляете всей этой свободой.
8bittree

48
Чтобы по-другому взглянуть на то, что сказал @ 8bittree, иметь публичный доступ - это хорошо, если люди, использующие ваш код, достаточно дисциплинированы, чтобы придерживаться заданного вами интерфейса. Конструкция частного члена возникла из-за людей, которые не могли держать свои носы там, где им не место.
Blrfl

20
Вы имели в виду «до того, как инкапсуляция стала популярной»? Инкапсуляция была довольно популярна до того, как языки OO стали популярными.
Фрэнк Хайлман

6
@FrankHileman Я думаю , что на самом деле суть вопроса: ОП хочет знать , если герметизация существовал на процедурных языках, перед тем Симула / Smalltalk / C ++
dcorking

18
Я заранее извиняюсь, если это звучит снисходительно, я не имею в виду это. Вам нужно выучить некоторые другие языки. Языки программирования предназначены не для машин, а для программистов . Они обязательно формируют то, как вы думаете. У вас просто не возникло бы этого вопроса, если бы вы потратили много времени на работу с JavaScript / Python / Ocaml / Clojure, даже если вы работали на Java весь день в своей повседневной работе. Кроме одного проекта с открытым исходным кодом на C ++, над которым я работаю (в основном это C), я с тех пор не использовал язык с модификаторами доступа и не пропустил их.
Джаред Смит

Ответы:


139

ООП не изобрел инкапсуляцию и не является синонимом инкапсуляции. Многие языки ООП не имеют модификаторов доступа в стиле C ++ / Java. Многие не-ООП языки имеют различные методы, доступные для инкапсуляции.

Один классический подход к инкапсуляции - это замыкания , используемые в функциональном программировании . Это значительно старше ООП, но в некотором смысле эквивалентно. Например, в JavaScript мы можем создать такой объект:

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

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

Вышеуказанный plus2объект не имеет члена, который бы позволял прямой доступ к нему x- он полностью инкапсулирован. add()Метод представляет собой замыкание по xпеременной.

Язык C поддерживает некоторые виды инкапсуляции посредством механизма заголовочных файлов , в частности, метод непрозрачного указателя . В C можно объявить имя структуры без определения ее членов. На этом этапе нельзя использовать переменную типа этой структуры, но мы можем свободно использовать указатели на эту структуру (поскольку размер указателя структуры известен во время компиляции). Например, рассмотрим этот заголовочный файл:

#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

Теперь мы можем написать код, который использует этот интерфейс Adder, не имея доступа к его полям, например:

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

И здесь будут полностью инкапсулированные детали реализации:

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

Существует также класс модульных языков программирования , который фокусируется на интерфейсах уровня модуля. Языковая семья ML, вкл. OCaml включает интересный подход к модулям, называемым функторами . ООП затмило и в значительной степени отнесено к модульному программированию, но многие предполагаемые преимущества ООП больше касаются модульности, чем объектной ориентации.

Также есть наблюдение, что классы в языках ООП, таких как C ++ или Java, часто используются не для объектов (в смысле сущностей, которые разрешают операции посредством позднего связывания / динамической диспетчеризации), а просто для абстрактных типов данных (где мы определяем открытый интерфейс, который скрывает детали внутренней реализации). В статье « Понимание абстракции данных» (Cook, 2009) обсуждается это различие более подробно.

Но да, во многих языках вообще нет механизма инкапсуляции. На этих языках члены структуры остаются открытыми. Самое большее, соглашение об именах будет препятствовать использованию. Например, я думаю, что у Паскаля не было полезного механизма инкапсуляции.


11
Видите ошибку в Adder self = malloc(sizeof(Adder));? Есть причина, указывающая на указатели типа, и на sizeof(TYPE)них обычно не одобряют.
Дедупликатор

10
Вы не можете просто написать sizeof(*Adder), потому что *Adderэто не тип, так же как *int *и не тип. Выражение T t = malloc(sizeof *t)идиоматическое и правильное. Смотрите мое редактирование.
wchargin

4
У Паскаля были переменные, которые нельзя было увидеть снаружи этой единицы. Фактически единичные переменные были эквивалентны private staticпеременным в Java. Аналогично C вы можете использовать непрозрачные указатели для передачи данных в Pascal без объявления того, что это было. Классический MacOS использовал множество непрозрачных указателей, поскольку открытые и закрытые части записи (структура данных) могут передаваться вместе. Я помню, что Window Manager делал это много, так как части Window Record были общедоступными, но также была включена некоторая внутренняя информация.
Михаил Шопсин

6
Возможно, лучшим примером, чем Pascal, является Python, который поддерживает объектную ориентацию, но не инкапсулирует, прибегая к соглашениям об именах, таким как _private_memberи output_property_, или к более продвинутым методам создания неизменяемых объектов.
Мефи

11
В литературе по OOD есть досадная тенденция представлять каждый принцип проектирования как принцип проектирования OO . (Неакадемическая) литература по ООД имеет тенденцию рисовать картину «темных веков», когда все делали все неправильно, а затем практикующие ООП освещают ситуацию. Насколько я могу судить, это в основном происходит от невежества. Например, насколько я могу судить, Боб Мартин серьезно взглянул на функциональное программирование всего несколько лет назад.
Дерек Элкинс

31

Во-первых, процедурный или объектно-ориентированный не имеет ничего общего с публичным против частного. Многие объектно-ориентированные языки не имеют понятия управления доступом.

Во-вторых, в «C» - который большинство людей назвали бы процедурным, а не объектно-ориентированным, есть много приемов, которые вы можете использовать, чтобы эффективно сделать вещи частными. Очень распространенным является использование непрозрачных (например, void *) указателей. Или - вы можете заранее объявить объект и просто не определять его в заголовочном файле.

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

Посмотрите на Windows SDK! Он использует HANDLE и UINT_PTR, и тому подобные вещи, чтобы быть общими дескрипторами памяти, используемой в API - эффективно делая реализации частными.


1
Мой образец продемонстрировал лучший (C) подход - использование заранее объявленных структур. чтобы использовать подход void *, я бы использовал typedefs: в файле .h говорите typedef void * queue, и везде, где у нас была структура queue, просто говорите queue; Затем в файле .c переименуйте struct queue в struct queueImpl, и все аргументы станут очередями (вместо struct queue *), а первой строкой кода для каждой такой функции станет struct queueImpl * qi = (struct queueImpl *) q
Lewis Прингл

7
Хм. Это делает его закрытым, потому что вы не можете получить доступ (читать или писать) к любым полям «очереди» из любого места, кроме его реализации (файл foo.c). Что еще ты имел в виду под частным? КСТАТИ - это верно для ОБА, подход typedef void * apporach и (лучше) форвард объявить struct
Льюис Прингл

5
Я должен признаться, что прошло уже почти 40 лет с тех пор, как я прочитал книгу о smalltalk-80, но я не припоминаю никаких представлений о публичных или частных членах данных. Я думаю, что CLOS также не имел такого понятия. Объект Паскаль не имел такого понятия. Я помню, что Simula сделала (возможно, там, где Stroustrup получил идею), и большинство языков OO, начиная с C ++, имеют это. В любом случае - мы согласны с тем, что инкапсуляция и личные данные - это хорошие идеи. Даже первоначальный вопросник был ясен по этому вопросу. Он просто спрашивал - как старые делали инкапсуляцию в языках до C ++.
Льюис Прингл

5
@LewisPringle: в Smalltalk-80 нет упоминания об открытых элементах данных, потому что все «переменные экземпляра» (члены данных) являются частными, если вы не используете отражение. AFAIU Smalltalkers пишут средства доступа для каждой переменной, которую они хотят сделать общедоступной.
1911 года

4
@LewisPringle, напротив, все «методы» Smalltalk (члены-функции) являются общедоступными (существуют неуклюжие соглашения о том, чтобы пометить их как частные)
dcorking

13

«Непрозрачные типы данных» были хорошо известной концепцией, когда я получил степень по информатике 30 лет назад. Мы не охватывали ООП, поскольку в то время оно не использовалось широко, и «функциональное программирование» считалось более правильным.

Modula-2 имела прямую поддержку для них, см. Https://www.modula2.org/reference/modules.php .

Льюис Прингл уже объяснил, как в C. можно использовать объявление о структуре в отличие от модуля-2, для создания объекта требовалась фабричная функция. ( Виртуальные методы также было легко реализовать в C , так как первый член структуры был указателем на другую структуру, которая содержала указатели на методы.)

Часто соглашение также использовалось. Например, к любому полю, начинающемуся с «_», нельзя обращаться за пределы файла, которому принадлежат данные. Это было легко обеспечено созданием пользовательских инструментов проверки.

В каждом крупномасштабном проекте, над которым я работал (до перехода на C ++, затем на C #), была система, предотвращающая доступ к «частным» данным из-за неправильного кода. Это было чуть менее стандартизировано, чем сейчас.


9

Обратите внимание, что существует много языков OO без встроенной возможности помечать участников как приватные. Это может быть сделано по соглашению, без необходимости для компилятора обеспечивать конфиденциальность. Например, люди часто ставят префикс частных переменных с подчеркиванием.

Существуют методы, которые затрудняют доступ к «закрытым» переменным, наиболее распространенным из которых является идиома PIMPL . Это помещает ваши приватные переменные в отдельную структуру, с указателем в ваших публичных заголовочных файлах. Это означает дополнительную разыменованность и приведение для получения любых частных переменных, что-то вроде этого ((private_impl)(obj->private))->actual_value, что раздражает, поэтому на практике редко используется.


4

Структуры данных не имели «членов», только поля данных (при условии, что это был тип записи). Видимость обычно была установлена ​​для всего типа. Однако это может быть не таким ограничивающим, как вы думаете, потому что функции не были частью записи.

Давайте вернемся назад и получим немного истории здесь ...

Доминирующая парадигма программирования до ООП называлась структурным программированием . Первоначальная главная цель этого состояла в том, чтобы избежать использования неструктурированных операторов перехода ("goto"). Это парадигма, ориентированная на поток управления (в то время как ООП в большей степени ориентирована на данные), но она все еще оставалась естественным продолжением попытки сохранить данные логически структурированными, как в коде.

Другим следствием структурированного программирования было сокрытие информации , идея о том, что реализации структуры кода (которая может изменяться довольно часто) должны храниться отдельно от интерфейса (который в идеале не будет меняться почти так же сильно). Это догма сейчас, но в былые времена, многие люди на самом деле считали лучше для каждого разработчика , чтобы узнать подробности о всей системе, так что это было в свое время на самом деле спорная идея. Оригинальное издание « Мистического человека за месяц» Брука фактически выступало против сокрытия информации.

Более поздние языки программирования, явно предназначенные для того, чтобы быть хорошими языками структурированного программирования (например, Modula-2 и Ada), обычно включали в себя скрытую информацию как фундаментальную концепцию, построенную вокруг некоторой концепции связного средства функций (и любых типов, констант и объекты, которые они могут потребоваться). В Модуле-2 их называли «Модулями», в Аде - «Пакетами». Многие современные языки ООП называют одну и ту же концепцию "пространства имен". Эти пространства имен были организационной основой разработки на этих языках, и для большинства целей могли использоваться аналогично классам ООП (конечно, без реальной поддержки наследования).

Таким образом, в Modula-2 и Ada (83) вы можете объявить любую подпрограмму, тип, константу или объект в пространстве имен частным или общедоступным, но если у вас был тип записи, не было (простого) способа объявить некоторые поля записи общедоступными. и другие частные. Либо вся ваша запись является публичной, либо нет.


Я провел довольно много времени, работая в Аде. Выборочное сокрытие (части типа данных) было чем-то, что мы делали все время; в пакете с содержимым вы сами определяете тип как закрытый или ограниченный; интерфейс пакета предоставит открытые функции / процедуры для получения и / или установки внутренних полей. Эти подпрограммы, конечно, должны принимать параметр закрытого типа. Я не тогда и не сейчас считаю это сложным.
Дэвид

Кроме того, AFAIK большинство OO-языков работает таким же образом, то есть myWidget.getFoo () действительно реализован как getFoo (myWidget). Это object.method()просто синтаксический сахар. Важно ИМХО - см. Принцип единого доступа / ссылки Майера - но все же просто синтаксический сахар.
Дэвид

@ Давид - это был аргумент сообщества Ада в течение многих лет в течение эры Ады 95. Я полагаю, что они наконец сдались и доказали свой собственный аргумент, допуская object.method()в качестве альтернативной формы method(object, ...) людей, которые просто не могли сделать концептуальный скачок.
Тед

0

В Си вы уже могли передавать указатели на объявленные, но неопределенные типы, как говорили другие, фактически ограничивая доступ ко всем полям.

Вы также можете иметь частные и публичные функции в зависимости от модуля. Функции, объявленные как статические в исходном файле, не видны снаружи, даже если вы пытаетесь угадать их имя. Точно так же вы можете иметь статические глобальные переменные на уровне файлов, что обычно является плохой практикой, но допускает изоляцию на модульной основе.

Вероятно, важно подчеркнуть, что ограничение доступа в качестве хорошо стандартизированного соглашения, а не навязанной языком конструкции работает просто отлично (см. Python). Кроме того, ограничение доступа к полям объекта может защитить программиста только тогда, когда необходимо изменить значение данных внутри объекта после создания. Который уже пахнет кодом. Можно утверждать, что C и, в частности, constключевое слово C ++ для методов и аргументов функций - гораздо более полезная помощь для программиста, чем довольно слабая Java final.


Единственная функция C, которая была специально предназначена для сокрытия информации, была staticглобальными данными и операциями (что означало, что они не были представлены компоновщику для использования из других компиляций). Вы можете правдоподобно утверждать, что любая поддержка, которую C оказывал для хороших практик проектирования программного обеспечения, кроме того, что это было в значительной степени хаком, и не являлось частью оригинального дизайна языка еще в 1972 году.
TED

0

Если ваше определение Public - это возможность доступа к реализации и данным / свойствам через ваш собственный код в любой момент, ответ прост: да . Тем не менее, он был абстрагирован различными способами - в зависимости от языка.

Я надеюсь, что это кратко ответил на ваш вопрос.


-1

Вот очень простой контрпример: в Java interfaces определяют объекты, а classes - нет. A classопределяет абстрактный тип данных, а не объект.

Следовательно, каждый раз, когда вы используете privateв classJava, у вас есть пример структуры данных с закрытыми членами, которая не является объектно-ориентированной.


7
Этот ответ, конечно, технически правильный, но он абсолютно непонятен любому, кто еще не знает, что такое ADT и чем они отличаются от объектов.
Амон

1
Я узнал кое-что из этого ответа.
малоО

3
Интерфейсы не «определяют» объекты; они определяют контракты для операций / поведения, которые объекты могут выполнять или выполнять. Так же , как наследование , как правило , описывается это соотношение и состав по имеет отношения, интерфейсы , как правило , описываются может сделать отношения.
code_dredd
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.