Я попал в интересный интернет-спор о методах получения и установки и инкапсуляции. Кто-то сказал, что все, что они должны сделать - это присваивание (установщики) или доступ к переменным (получатели), чтобы сохранить их «чистыми» и обеспечить инкапсуляцию.
- Прав ли я, что это полностью разрушило бы цель иметь в первую очередь геттеры и сеттеры, а валидация и другая логика (без странных побочных эффектов, конечно) должны быть разрешены?
- Когда должна произойти проверка?
- При установке значения внутри сеттера (чтобы защитить объект от попадания в недопустимое состояние - моё мнение)
- Перед установкой значения вне установщика
- Внутри объекта, перед каждым использованием значения
- Разрешено ли установщику изменять значение (возможно, преобразовать допустимое значение в какое-то каноническое внутреннее представление)?
object-oriented
language-agnostic
encapsulation
Ботонд Балаш
источник
источник
Ответы:
Я помню подобные споры с моим лектором при изучении C ++ в университете. Я просто не мог понять смысл использования геттеров и сеттеров, когда я мог сделать переменную общедоступной. Теперь я понимаю лучше с многолетним опытом, и я узнал более вескую причину, чем просто сказать «поддерживать инкапсуляцию».
Определив методы получения и установки, вы обеспечите согласованный интерфейс, так что если вы захотите изменить свою реализацию, у вас будет меньше шансов нарушить зависимый код. Это особенно важно, когда ваши классы предоставляются через API и используются в других приложениях или третьими лицами. Так как насчет того, что входит в геттер или сеттер?
Получатели обычно лучше реализованы как простой скрытый проход для доступа к значению, потому что это делает их поведение предсказуемым. Я говорю вообще, потому что я видел случаи, когда геттеры использовались для доступа к значениям, управляемым вычислением или даже условным кодом. Как правило, не очень хорошо, если вы создаете визуальные компоненты для использования во время разработки, но, казалось бы, удобно во время выполнения. Однако нет реальной разницы между этим и использованием простого метода, за исключением того, что когда вы используете метод, вы, как правило, с большей вероятностью будете называть метод более подходящим образом, чтобы функциональность «получателя» была более очевидной при чтении кода.
Сравните следующее:
а также
Вторая опция проясняет, что значение вычисляется, тогда как первый пример говорит вам, что вы просто возвращаете значение, ничего не зная о самом значении.
Возможно, вы могли бы утверждать, что следующее будет более понятным:
Проблема, однако, в том, что вы предполагаете, что значением уже манипулировали в другом месте. Таким образом, в случае получателя, хотя вы можете предположить, что при возврате значения может происходить что-то еще, трудно сделать такие вещи понятными в контексте свойства, и имена свойств никогда не должны содержать глаголов. в противном случае сложно сразу понять, следует ли при обращении использовать используемое имя в скобках.
Однако сеттеры - это немного другой случай. Вполне уместно, чтобы установщик обеспечивал некоторую дополнительную обработку для проверки данных, передаваемых в свойство, исключая исключение, если установка значения будет нарушать определенные границы свойства. Проблема, с которой сталкиваются некоторые разработчики при добавлении обработки к сеттерам, заключается в том, что всегда есть соблазн заставить сеттера сделать немного больше, например, выполнить вычисления или манипулировать данными каким-либо образом. Здесь вы можете получить побочные эффекты, которые в некоторых случаях могут быть непредсказуемыми или нежелательными.
В случае с сеттерами я всегда применяю простое эмпирическое правило, которое заключается в том, чтобы как можно меньше делать с данными. Например, я обычно разрешаю либо граничное тестирование, либо округление, чтобы я мог вызывать исключения, если это уместно, или избегать ненужных исключений, где их можно разумно избегать. Свойства с плавающей запятой являются хорошим примером, когда вы можете захотеть округлить чрезмерные десятичные разряды, чтобы избежать возникновения исключения, но при этом разрешить ввод значений диапазона с несколькими дополнительными десятичными разрядами.
Если вы применяете какие-то манипуляции с входом установщика, у вас возникает та же проблема, что и с геттером, из-за которой другим сложно понять, что делает установщик, просто назвав его. Например:
Говорит ли это что-нибудь о том, что произойдет со значением, когда оно передается сеттеру?
Как насчет:
Второй пример точно сообщает вам, что произойдет с вашими данными, а первый не даст вам знать, будет ли ваше значение произвольно изменено. При чтении кода второй пример будет намного понятнее по назначению и функциям.
Наличие методов получения и установки имеет значение не для инкапсуляции ради «чистоты», а для инкапсуляции, чтобы можно было легко реорганизовать код без риска изменения интерфейса класса, который в противном случае нарушил бы совместимость класса с вызывающим кодом. Валидация полностью уместна в установщике, однако существует небольшой риск того, что изменение в валидации может нарушить совместимость с вызывающим кодом, если вызывающий код зависит от валидации, происходящей определенным образом. Это, как правило, редкая ситуация с относительно низким уровнем риска, но ее следует отметить для полноты картины.
Проверка должна происходить в контексте установщика до фактической установки значения. Это гарантирует, что в случае возникновения исключения состояние вашего объекта не изменится и потенциально лишит законной силы его данные. Я обычно считаю, что лучше делегировать валидацию отдельному методу, который будет первым, что вызывается в установщике, для того, чтобы код установщика был относительно беспорядочным.
В очень редких случаях, может быть. В общем, вероятно, лучше не делать этого. Такого рода вещи лучше оставить другому методу.
источник
Если получатель / установщик просто отражает значение, то нет смысла иметь их или делать значение частным. Нет ничего плохого в том, чтобы обнародовать некоторые переменные-члены, если у вас есть веская причина. Если вы пишете класс 3D-точек, то иметь открытый доступ к .x, .y, .z имеет смысл.
Как сказал Ральф Уолдо Эмерсон: «Глупая последовательность - это хобгоблин маленьких умов, обожаемый маленькими государственными деятелями, философами и дизайнерами Java».
Методы получения / установки полезны, когда могут быть побочные эффекты, когда вам нужно обновить другие внутренние переменные, пересчитать кэшированные значения и защитить класс от недопустимых входных данных.
Обычное оправдание для них, что они скрывают внутреннюю структуру, обычно наименее полезно. например. У меня эти точки хранятся как 3 числа с плавающей запятой, я мог бы решить сохранить их как строки в удаленной базе данных, поэтому я сделаю методы получения / установки, чтобы скрыть их, как если бы вы могли сделать это без какого-либо другого влияния на код вызывающего.
источник
IEnumerable<T>
чем принудительно устанавливать его как-то такList<T>
. Я бы сказал, что ваш пример доступа к базе данных нарушает единственную ответственность - смешивает представление модели с тем, как она сохраняется.Принцип унифицированного доступа Мейера: «Все сервисы, предлагаемые модулем, должны быть доступны через унифицированную нотацию, в которой не указано, реализованы ли они посредством хранения или вычислений». является основной причиной получения / установки, иначе свойства.
Если вы решите кэшировать или лениво вычислить одно поле класса, вы можете изменить это в любое время, если у вас есть только открытые средства доступа к свойствам, а не конкретные данные.
Объекты-значения, простые структуры не нуждаются в этой абстракции, а полноценный класс, на мой взгляд, в этом нуждается.
источник
Распространенной стратегией проектирования классов, которая была представлена на языке Eiffel, является разделение команд и запросов . Идея состоит в том, что метод должен либо сообщать вам что-то об объекте, либо указывать объекту что-то делать, но не делать то и другое.
Это относится только к общедоступному интерфейсу класса, а не к внутреннему представлению. Рассмотрим объект модели данных, который поддерживается строкой в базе данных. Вы можете создать объект без загрузки данных, а затем, при первом вызове метода получения, он действительно выполняет
SELECT
. Это нормально, вы можете изменять некоторые внутренние детали того, как представлен объект, но вы не меняете то, как это выглядит для клиентов этого объекта. Вы должны иметь возможность вызывать геттеры несколько раз и при этом получать одинаковые результаты, даже если они выполняют различную работу, чтобы вернуть эти результаты.Точно так же сеттер выглядит контрактно, как будто он просто меняет состояние объекта. Это может сделать это каким-то запутанным способом - запись
UPDATE
в базу данных или передача параметра в некоторый внутренний объект. Это нормально, но делать что-то не связанное с установкой состояния было бы удивительно.Мейер (создатель Eiffel) также хотел сказать о проверке. По сути, всякий раз, когда объект находится в состоянии покоя, он должен быть в допустимом состоянии. Таким образом, сразу после завершения конструктора, до и после (но не обязательно во время) каждого вызова внешнего метода, состояние объекта должно быть согласованным.
Интересно отметить, что на этом языке синтаксис для вызова процедуры и для чтения открытого атрибута выглядит одинаково. Другими словами, вызывающая сторона не может сказать, используют ли они какой-либо метод или работают напрямую с переменной экземпляра. Это только в языках, которые не скрывают эту деталь реализации, где даже возникает вопрос - если вызывающие не могут сказать, вы можете переключаться между общедоступным ivar и аксессорами, не пропуская это изменение в клиентский код.
источник
Другая приемлемая вещь - клонирование. В некоторых случаях вам нужно убедиться, что после того, как кто-то дает ваш класс, например. список чего-либо, он не может изменить его внутри вашего класса (или изменить объект в этом списке). Поэтому вы делаете глубокую копию параметра в сеттере и возвращаете глубокую копию в геттер. (Использование неизменяемых типов в качестве параметров - еще один вариант, но выше предполагается, что это невозможно). Но не клонируйте средства доступа, если это не нужно. Легко подумать (но не правильно) о свойствах / получателях и установщиках как операциях с постоянными затратами, так что это предельная производительность, ожидающая пользователя API.
источник
Не всегда отображается соотношение 1-1 между средствами доступа к свойствам и иварами, в которых хранятся данные.
Например, класс представления может предоставлять
center
свойство, даже если нет ивара, в котором хранится центр представления; установкаcenter
вызывает изменения в других ivars, напримерorigin
илиtransform
что-то еще, но клиенты класса не знают или не заботятся о том, какcenter
хранится, при условии, что он работает правильно. Однако чего не должно происходить, так это того, что настройкаcenter
заставляет вещи происходить сверх того, что необходимо для сохранения нового значения, как бы это ни было.источник
Самое лучшее в установщиках и получателях заключается в том, что они позволяют легко изменять правила API без изменения API. Если вы обнаружите ошибку, гораздо более вероятно, что вы сможете исправить ошибку в библиотеке и не каждый потребитель обновит свою кодовую базу.
источник
Я склонен полагать, что сеттеры и геттеры являются злыми и должны использоваться только в классах, которые управляются фреймворком / контейнером. Правильный дизайн класса не должен давать геттеры и сеттеры.
Редактировать: хорошо написанная статья на эту тему .
Edit2: открытые поля - это нонсенс в подходе ООП; говоря, что геттеры и сеттеры - это зло, я не имею в виду, что они должны быть заменены публичными полями.
источник