Что должно быть разрешено внутри геттеров?

45

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

  • Прав ли я, что это полностью разрушило бы цель иметь в первую очередь геттеры и сеттеры, а валидация и другая логика (без странных побочных эффектов, конечно) должны быть разрешены?
  • Когда должна произойти проверка?
    • При установке значения внутри сеттера (чтобы защитить объект от попадания в недопустимое состояние - моё мнение)
    • Перед установкой значения вне установщика
    • Внутри объекта, перед каждым использованием значения
  • Разрешено ли установщику изменять значение (возможно, преобразовать допустимое значение в какое-то каноническое внутреннее представление)?
Ботонд Балаш
источник
18
Лучшее в методах получения и установки - это возможность их не иметь , т. Е. Выход из установщика, и у вас есть свойство только для чтения, выход из получателя, и у вас есть опция конфигурации, текущее значение которой никому не нужно.
Майкл Боргвардт
7
@MichaelBorgwardt Оставьте оба, чтобы иметь чистый интерфейс «говори, не спрашивай». Свойства = потенциальный запах кода.
Конрад Рудольф
Обязательная ссылка: javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
user281377
2
Сеттеры также могут быть использованы для поднятия События .
Джон Исайя Кармона
@KonradRudolph Я согласен с вашим утверждением, хотя я хотел бы действительно подчеркнуть слово «потенциал».
Фил

Ответы:

37

Я помню подобные споры с моим лектором при изучении C ++ в университете. Я просто не мог понять смысл использования геттеров и сеттеров, когда я мог сделать переменную общедоступной. Теперь я понимаю лучше с многолетним опытом, и я узнал более вескую причину, чем просто сказать «поддерживать инкапсуляцию».

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

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

Сравните следующее:

int aValue = MyClass.Value;

а также

int aValue = MyClass.CalculateValue();

Вторая опция проясняет, что значение вычисляется, тогда как первый пример говорит вам, что вы просто возвращаете значение, ничего не зная о самом значении.

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

int aValue = MyClass.CalculatedValue;

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

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

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

Если вы применяете какие-то манипуляции с входом установщика, у вас возникает та же проблема, что и с геттером, из-за которой другим сложно понять, что делает установщик, просто назвав его. Например:

MyClass.Value = 12345;

Говорит ли это что-нибудь о том, что произойдет со значением, когда оно передается сеттеру?

Как насчет:

MyClass.RoundValueToNearestThousand(12345);

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

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

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

Когда должна произойти проверка?

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

Разрешено ли установщику изменять значение (возможно, преобразовать допустимое значение в какое-то каноническое внутреннее представление)?

В очень редких случаях, может быть. В общем, вероятно, лучше не делать этого. Такого рода вещи лучше оставить другому методу.

S.Robins
источник
Если вы измените значение, было бы разумно установить его на -1 или какой-либо токен NULL-флага, если установщик передал недопустимое значение
Martin Beckett
1
Есть несколько проблем с этим. Произвольная установка значения создает намеренный и неясный побочный эффект. Кроме того, он не позволяет вызывающему коду получать обратную связь, которая может быть использована для лучшей обработки нелегальных данных. Это особенно важно для значений на уровне пользовательского интерфейса. Чтобы быть справедливым, одно исключение, о котором я только что подумал, могло бы состоять в том, чтобы разрешить ввод нескольких форматов даты при сохранении даты в стандартном формате даты / времени. Можно с уверенностью утверждать, что этот конкретный «побочный эффект» является нормализацией данных при проверке при условии, что входные данные являются законными.
С.Робинс
Да, одним из оправданий метода установки является то, что он должен возвращать true / false, если значение может быть установлено.
Мартин Беккет
1
«Свойства с плавающей запятой являются хорошим примером, когда вы можете захотеть округлить излишние десятичные разряды, чтобы избежать создания исключения». В каких случаях слишком большое количество десятичных разрядов вызывает исключение?
Марк Байерс
2
Я вижу изменение значения в каноническое представление как форму проверки. По умолчанию пропущенные значения (например, текущая дата, если временная метка содержит только время) или масштабирование значения (.93275 -> 93,28%), чтобы гарантировать, что внутренняя согласованность должна быть в порядке, но любые такие манипуляции должны быть явно вызваны в документации API особенно если они являются необычной идиомой в API.
TMN
20

Если получатель / установщик просто отражает значение, то нет смысла иметь их или делать значение частным. Нет ничего плохого в том, чтобы обнародовать некоторые переменные-члены, если у вас есть веская причина. Если вы пишете класс 3D-точек, то иметь открытый доступ к .x, .y, .z имеет смысл.

Как сказал Ральф Уолдо Эмерсон: «Глупая последовательность - это хобгоблин маленьких умов, обожаемый маленькими государственными деятелями, философами и дизайнерами Java».

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

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

Мартин Беккет
источник
1
Вау, дизайнеры Java божественны? </ jk>
@delnan - очевидно нет - или они лучше рифмуются ;-)
Мартин Беккет
Будет ли правильным создать трехмерную точку, в которой все x, y, z являются Float.NAN?
Эндрю Т Финнелл
4
Я не согласен с вашей точкой зрения, что «обычное обоснование» (скрытие внутренней структуры) является наименее полезным свойством геттеров и сеттеров. В C # определенно полезно сделать свойство интерфейсом, базовая структура которого может быть изменена - например, если вы хотите, чтобы свойство было доступно только для перечисления, это гораздо лучше сказать, IEnumerable<T>чем принудительно устанавливать его как-то так List<T>. Я бы сказал, что ваш пример доступа к базе данных нарушает единственную ответственность - смешивает представление модели с тем, как она сохраняется.
Брэндон Линтон
@AndrewFinnell - это может быть хорошим способом пометить точки как невозможные / недействительные / удаленные и т. Д.
Martin Beckett
8

Принцип унифицированного доступа Мейера: «Все сервисы, предлагаемые модулем, должны быть доступны через унифицированную нотацию, в которой не указано, реализованы ли они посредством хранения или вычислений». является основной причиной получения / установки, иначе свойства.

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

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

Карл
источник
2

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

Это относится только к общедоступному интерфейсу класса, а не к внутреннему представлению. Рассмотрим объект модели данных, который поддерживается строкой в ​​базе данных. Вы можете создать объект без загрузки данных, а затем, при первом вызове метода получения, он действительно выполняет SELECT. Это нормально, вы можете изменять некоторые внутренние детали того, как представлен объект, но вы не меняете то, как это выглядит для клиентов этого объекта. Вы должны иметь возможность вызывать геттеры несколько раз и при этом получать одинаковые результаты, даже если они выполняют различную работу, чтобы вернуть эти результаты.

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

Мейер (создатель Eiffel) также хотел сказать о проверке. По сути, всякий раз, когда объект находится в состоянии покоя, он должен быть в допустимом состоянии. Таким образом, сразу после завершения конструктора, до и после (но не обязательно во время) каждого вызова внешнего метода, состояние объекта должно быть согласованным.

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


источник
1

Другая приемлемая вещь - клонирование. В некоторых случаях вам нужно убедиться, что после того, как кто-то дает ваш класс, например. список чего-либо, он не может изменить его внутри вашего класса (или изменить объект в этом списке). Поэтому вы делаете глубокую копию параметра в сеттере и возвращаете глубокую копию в геттер. (Использование неизменяемых типов в качестве параметров - еще один вариант, но выше предполагается, что это невозможно). Но не клонируйте средства доступа, если это не нужно. Легко подумать (но не правильно) о свойствах / получателях и установщиках как операциях с постоянными затратами, так что это предельная производительность, ожидающая пользователя API.

user470365
источник
1

Не всегда отображается соотношение 1-1 между средствами доступа к свойствам и иварами, в которых хранятся данные.

Например, класс представления может предоставлять centerсвойство, даже если нет ивара, в котором хранится центр представления; установка centerвызывает изменения в других ivars, например originили transformчто-то еще, но клиенты класса не знают или не заботятся о том, как center хранится, при условии, что он работает правильно. Однако чего не должно происходить, так это того, что настройка centerзаставляет вещи происходить сверх того, что необходимо для сохранения нового значения, как бы это ни было.

Калеб
источник
0

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

AlexanderBrevig
источник
-4

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

Редактировать: хорошо написанная статья на эту тему .

Edit2: открытые поля - это нонсенс в подходе ООП; говоря, что геттеры и сеттеры - это зло, я не имею в виду, что они должны быть заменены публичными полями.

m3th0dman
источник
1
Я думаю, что вы упускаете смысл статьи, которая предлагает экономно использовать методы получения / установки и избегать показа данных класса без необходимости. Это ИМХО является разумным принципом проектирования, который должен сознательно применяться разработчиком. С точки зрения создания свойств в классе, вы можете просто представить переменную, но это усложняет обеспечение проверки, когда переменная установлена, или извлечение значения из альтернативного источника, когда «получено», по существу нарушая инкапсуляцию и потенциально запирая вас в трудно поддерживаемый дизайн интерфейса.
С.Робинс
@ S.Robins На мой взгляд, публичные добытчики и сеттеры не правы; публичные поля более неправильны (если это сопоставимо).
m3th0dman
@methodman Да, я согласен, что публичное поле неверно, однако публичное свойство может быть полезным. Это свойство может использоваться для предоставления места для проверки или событий, связанных с установкой или возвратом данных, в зависимости от конкретных требований в данный момент. Сами по себе добытчики и сеттеры не ошибаются. С другой стороны, то, как и когда они используются или злоупотребляются, может рассматриваться как плохое с точки зрения конструкции и ремонтопригодности в зависимости от обстоятельств. :)
С.Робинс
@ S.Robins Подумайте об основах ООП и моделирования; объекты должны копировать реальные объекты. Существуют ли реальные сущности с такими типами операций / свойств, такие как геттеры / сеттеры?
m3th0dman
Чтобы здесь не было слишком много комментариев, я перенесу этот разговор в этот чат и отправлю туда ваш комментарий.
С.Робинс