Частные и публичные члены на практике (насколько важна инкапсуляция?)

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

Мне любопытно, насколько важно это на практике. В частности, если у вас есть более сложный элемент (например, коллекция), может быть очень заманчиво просто сделать его общедоступным, а не создать набор методов для получения ключей коллекции, добавления / удаления элементов из коллекции, и т.п.

Соблюдаете ли вы правило в целом? Меняется ли ваш ответ в зависимости от того, написан ли этот код для вас против использования другими? Есть ли более тонкие причины, по которым я скучаю по этой путанице?

22 ответа

Решение

Это зависит. Это один из тех вопросов, который нужно решать прагматично.

Предположим, у меня был класс для представления точки. У меня могут быть геттеры и сеттеры для координат X и Y, или я могу просто сделать их открытыми и разрешить свободный доступ для чтения / записи данных. На мой взгляд, это нормально, потому что класс действует как прославленная структура - сбор данных, возможно, с некоторыми полезными функциями.

Однако существует множество обстоятельств, когда вы не хотите предоставлять полный доступ к своим внутренним данным и полагаться на методы, предоставляемые классом, для взаимодействия с объектом. Примером может быть HTTP-запрос и ответ. В этом случае плохая идея разрешать кому-либо отправлять что-либо по проводам - ​​это должно быть обработано и отформатировано методами класса. В этом случае класс рассматривается как реальный объект, а не как простое хранилище данных.

Это действительно сводится к тому, управляют ли глаголы (методы) структурой или данные делают.

Как кто-то, кто должен был поддерживать многолетний код, над которым работали многие люди в прошлом, мне совершенно ясно, что если атрибут member становится общедоступным, он в конечном итоге злоупотребляется. Я даже слышал, как люди не соглашались с идеей доступа и мутаторов, поскольку это все еще не соответствует цели инкапсуляции, которая заключается в "сокрытии внутренней работы класса". Это, очевидно, спорная тема, но, по моему мнению, "сделать каждую переменную-член приватной, прежде всего подумайте о том, что должен делать класс (методы), а не о том, как вы позволите людям изменять внутренние переменные".

Да, инкапсуляция имеет значение. Предоставление базовой реализации делает (по крайней мере) две вещи неправильно:

  1. Смешивает обязанности. Вызывающие абоненты не должны или не хотят понимать основную реализацию. Им просто нужно, чтобы класс выполнял свою работу. Предоставляя базовую реализацию, ваш класс не выполняет свою работу. Вместо этого это просто возлагает ответственность на абонента.
  2. Связывает вас с базовой реализацией. Как только вы выставите базовую реализацию, вы привязаны к ней. Если вы скажете вызывающим, например, что под ними есть коллекция, вы не сможете легко поменять коллекцию на новую реализацию.

Эти (и другие) проблемы применяются независимо от того, предоставляете ли вы прямой доступ к базовой реализации или просто дублируете все базовые методы. Вы должны выставлять необходимую реализацию, и ничего более. Сохранение частной реализации делает всю систему более удобной в обслуживании.

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

С уважением, Олли

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

Пример: я пишу код, который обновляет переменную, и мне нужно быть абсолютно уверенным, что Gui изменяется, чтобы отразить это изменение, самый простой способ - добавить метод доступа (он же называется "Setter"), который вызывается вместо Обновление данных обновляется.

Если я сделаю эти данные общедоступными, и что-то изменит переменную без прохождения метода Setter (а это происходит каждый раз, когда произносят нецензурные слова), то кому-то понадобится час отладки, чтобы выяснить, почему обновления не отображаются. То же самое относится в меньшей степени к "получению" данных. Я мог бы поместить комментарий в заголовочный файл, но есть вероятность, что никто не будет его читать, пока что-то не станет ужасно, ужасно неправильно. Применение его с помощью private означает, что ошибка не может быть допущена, потому что она будет отображаться как легко обнаруживаемая ошибка времени компиляции, а не ошибка времени выполнения.

Исходя из опыта, единственные случаи, когда вы захотите сделать переменную-член общедоступной и не использовать методы Getter и Setter, - это если вы хотите сделать абсолютно ясным, что ее изменение не будет иметь побочных эффектов; особенно если структура данных проста, как класс, который просто содержит две переменные в паре.

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

С учетом вышесказанного, для большинства небольших программ одноразового использования без расширения, например, тех, которые вы получаете в университете, это более "хорошая практика", чем что-либо еще, потому что вы будете помнить в процессе их написания, а затем Я дам их и никогда не трогать код снова. Кроме того, если вы пишете структуру данных как способ выяснить, как они хранят данные, а не как код выпуска, то есть хороший аргумент, что Getters и Setters не помогут, и будут мешать обучению,

Только когда вы доберетесь до рабочего места или большого проекта, где вероятность того, что ваш код будет вызываться объектами и структурами, написанными разными людьми, становится жизненно важным, чтобы эти "напоминания" были сильными. Является ли это проектом с одним человеком или нет, на удивление не имеет значения, по той простой причине, что "вы через шесть недель" - такой же человек, как и сотрудник. И "я шесть недель назад" часто оказывается ленивым.

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

Я стараюсь строго придерживаться правила, даже когда это просто мой собственный код. Мне очень нравятся свойства в C# по этой причине. Это позволяет легко контролировать, какие значения ему даны, но вы все равно можете использовать их в качестве переменных. Или сделайте сериал приватной, общедоступной и т. Д.

C# Свойства "имитируют" открытые поля. Выглядит довольно круто, и синтаксис действительно ускоряет создание этих методов get/set

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

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

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


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

@VonC

Вы можете найти интересную статью Международной организации по стандартизации "Эталонная модель открытой распределенной обработки". Он определяет: "Инкапсуляция: свойство, что информация, содержащаяся в объекте, доступна только через взаимодействия на интерфейсах, поддерживаемых объектом".

Я попытался доказать, что сокрытие информации является важной частью этого определения здесь: http://www.edmundkirwan.com/encap/s2.html

С Уважением,

Издание

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

Наличие обоих методов получения и установки равно общему полю (когда методы получения и установки тривиальны / генерируются автоматически). Иногда может быть лучше просто объявить поля общедоступными, чтобы код был более простым, если только вам не нужен полиморфизм или среда не требует методов get/set (и вы не можете изменить структуру).

Но есть также случаи, когда наличие геттеров и сеттеров - хороший пример. Один пример:

Когда я создаю GUI приложения, я пытаюсь сохранить поведение GUI в одном классе (FooModel), чтобы его можно было легко тестировать модулем, и визуализировать GUI в другом классе (FooView), который можно протестировать. только вручную. Вид и модель объединены простым клеевым кодом; когда пользователь меняет значение поля xЗвонит вид setX(String) на модели, которая, в свою очередь, может вызвать событие, что какая-то другая часть модели была изменена, и представление получит обновленные значения из модели с геттерами.

В одном проекте есть модель с графическим интерфейсом, которая имеет 15 методов получения и установки, из которых только три метода получения являются тривиальными (так что среда IDE может их сгенерировать). Все остальные содержат некоторые функциональные или нетривиальные выражения, такие как следующее:

public boolean isEmployeeStatusEnabled() {
    return pinCodeValidation.equals(PinCodeValidation.VALID);
}

public EmployeeStatus getEmployeeStatus() {
    Employee employee;
    if (isEmployeeStatusEnabled()
            && (employee = getSelectedEmployee()) != null) {
        return employee.getStatus();
    }
    return null;
}

public void setEmployeeStatus(EmployeeStatus status) {
    getSelectedEmployee().changeStatusTo(status, getPinCode());
    fireComponentStateChanged();
}

Когда я делаю объекты значимыми, их легче использовать и легче поддерживать.

Например: Person.Hand.Grab(howquick, howmuch);

Хитрость заключается не в том, чтобы думать о членах как о простых ценностях, а об объектах в себе.

Приспособьте инструмент к работе... недавно я увидел такой код в моей текущей кодовой базе:

private static class SomeSmallDataStructure {
    public int someField;
    public String someOtherField;
}

И затем этот класс был использован для внутренней передачи нескольких значений данных. Это не всегда имеет смысл, но если у вас есть только ДАННЫЕ без методов, и вы не предоставляете их клиентам, я нахожу это довольно полезным шаблоном.

Самым последним моим использованием была страница JSP, на которой отображалась таблица данных, определенная в верхней части декларативно. Итак, изначально он был в нескольких массивах, по одному массиву на поле данных... это закончилось тем, что в коде было довольно трудно разбираться с полями, не находящимися рядом друг с другом по определению, которые будут отображаться вместе... поэтому я создал простой класс как выше, который собрал бы это вместе... результат был ДЕЙСТВИТЕЛЬНО читаемым кодом, намного больше чем прежде.

Мораль... иногда вам следует рассмотреть "принятые плохие" альтернативы, если они могут сделать код проще и легче для чтения, если вы продумываете его и учитываете последствия... не принимайте вслепую ВСЕ, что слышите.

Тем не менее, публичные геттеры и сеттеры в значительной степени эквивалентны публичным полям... по крайней мере, по существу (есть немного большая гибкость, но все равно это плохой шаблон для применения к КАЖДОМУ полю, который у вас есть).

Даже в стандартных библиотеках java есть несколько открытых полей.

Все дело в контроле того, что люди могут делать с тем, что вы им даете. Чем больше вы контролируете, тем больше допущений вы можете сделать.

Кроме того, теоретически вы можете изменить базовую реализацию или что-то, но так как по большей части это:

private Foo foo;
public Foo getFoo() {}
public void setFoo(Foo foo) {}

Это немного сложно оправдать.

Инкапсуляция важна, когда хотя бы один из них имеет место:

  1. Любой, кроме вас, будет использовать ваш класс (или они сломают ваши инварианты, потому что они не читают документацию).
  2. Любой, кто не читает документацию, собирается использовать ваш класс (или они сломают ваши тщательно задокументированные инварианты). Обратите внимание, что эта категория включает вас через два года.
  3. В какой-то момент в будущем кто-то собирается унаследовать от вашего класса (потому что, возможно, при изменении значения поля необходимо предпринять дополнительное действие, поэтому должен быть установщик).

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

Моя тенденция состоит в том, чтобы попытаться сделать все конфиденциальным, если это возможно. Это позволяет максимально четко определять границы объектов и максимально разъединять объекты. Мне это нравится, потому что, когда мне приходится переписывать объект, который я испортил первый (второй, пятый?) Раз, он сохраняет урон, нанесенный меньшему количеству объектов.

Если вы соединяете объекты достаточно плотно, может быть проще объединить их в один объект. If you relax the coupling constraints enough you're back to structured programming.

It may be that if you find that a bunch of your objects are just accessor functions, you should rethink your object divisions. If you're not doing any actions on that data it may belong as a part of another object.

Of course, if you're writing a something like a library you want as clear and sharp of an interface as possible so others can program against it.

Я бы сказал, что этот вопрос смешивает концепцию инкапсуляции с "сокрытием информации".
(это не критик, поскольку, похоже, оно соответствует общей интерпретации понятия "инкапсуляция")

Однако для меня "инкапсуляция" это либо:

  • процесс перегруппировки нескольких предметов в контейнер
  • сам контейнер перегруппировывает предметы

Предположим, вы разрабатываете систему налогоплательщиков. Для каждого налогоплательщика вы можете включить понятие ребенка в

  • список детей, представляющих детей
  • карта с учетом детей от разных родителей
  • объект Children (не Child), который предоставит необходимую информацию (например, общее количество детей)

Здесь у вас есть три различных вида инкапсуляции, 2 представленные контейнером нижнего уровня (список или карта), один представлен объектом.

Принимая эти решения, вы не

  • сделать эту инкапсуляцию общедоступной, защищенной или закрытой: выбор "сокрытия информации" еще предстоит сделать
  • сделать полную абстракцию (вам нужно уточнить атрибуты объекта Children, и вы можете решить создать объект Child, который будет хранить только релевантную информацию с точки зрения системы налогоплательщиков)
    Абстракция - это процесс выбора того, какие атрибуты объекта имеют отношение к вашей системе, а какие должны быть полностью проигнорированы.

Итак, моя точка зрения такова:
Этот вопрос может быть озаглавлен:
Частные против публичных членов на практике (насколько важна скрытие информации?)

Только мои 2 цента, хотя. Я совершенно уважаю, что можно рассматривать инкапсуляцию как процесс, включающий решение о "сокрытии информации".

Тем не менее, я всегда стараюсь различать "абстракция" - "инкапсуляция" - "скрытие информации или видимость".

Здесь есть практическая проблема, которая не решается большинством существующих ответов. Инкапсуляция и доступ к чистым, безопасным интерфейсам для внешнего кода всегда велики, но гораздо важнее, когда код, который вы пишете, предназначен для использования пространственно и / или временно большой пользовательской базой. Я имею в виду, что если вы планируете, чтобы кто-то (даже вы) поддерживал код в будущем, или если вы пишете модуль, который будет взаимодействовать с кодом от более чем нескольких других разработчиков, вам нужно думать гораздо больше. Тщательно, чем если бы вы писали код, который либо одноразовый, либо полностью написан вами.

Честно говоря, я знаю, что это за ужасная практика разработки программного обеспечения, но сначала я часто делаю все публично, что делает вещи немного быстрее, чтобы их запоминать и печатать, затем добавляю инкапсуляцию, как это имеет смысл. Рефакторинг инструментов в большинстве популярных IDE в наши дни делает ваш подход (добавление инкапсуляции вместо удаления) гораздо менее актуальным, чем раньше.

На практике я всегда следую только одному правилу - правилу "нет размера, который подходит всем".

Инкапсуляция и ее важность является продуктом вашего проекта. Какой объект будет иметь доступ к вашему интерфейсу, как они будут его использовать, будет ли иметь значение, если у них будут лишние права доступа к членам? Эти вопросы и подобные им вы должны задать себе при работе над каждой реализацией проекта.

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

Это, как говорится, мне нравится способ Python справиться с этим. Переменные-члены являются открытыми по умолчанию. Если вы хотите скрыть их или добавить валидацию, существуют методы, но они рассматриваются как особые случаи.

Конечно, это имеет значение, если вы пишете внутренний код или код, который будет использоваться кем-то другим (или даже самим собой, но как отдельный элемент). Любой код, который будет использоваться снаружи, должен иметь четко определенный / документированный интерфейс, который вы хочу поменять как можно меньше.

Для внутреннего кода, в зависимости от сложности, вы можете обнаружить, что теперь проще делать что-то простым, а потом заплатить небольшой штраф. Конечно, закон Мерфи будет гарантировать, что краткосрочный выигрыш будет многократно стираться при необходимости внесения более широких изменений позже, когда вам нужно было изменить внутренние компоненты класса, которые вы не смогли инкапсулировать.

Я основываю свое решение на глубине Кодекса в модуле. Если я пишу код, который является внутренним для модуля и не взаимодействует с внешним миром, я не столько инкапсулирую вещи в private, поскольку это влияет на производительность моего программиста (насколько быстро я могу писать и переписывать свой код).

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

Я следую правилам на этом почти все время. Для меня есть четыре сценария - в основном, само правило и несколько исключений (все под влиянием Java):

  1. Используется любым объектом вне текущего класса, доступ к которому осуществляется через методы получения / установки
  2. Использование внутри класса, как правило, предшествует 'this', чтобы прояснить, что это не параметр метода
  3. Что-то означало оставаться очень маленьким, как транспортный объект - в основном прямой набор атрибутов; все общественное
  4. Должен быть не частным для расширения какого-либо рода
Другие вопросы по тегам