Я помню подобные споры с моим лектором при изучении C ++ в университете. Я просто не мог понять смысл использования геттеров и сеттеров, когда я мог сделать переменную общедоступной. Теперь я понимаю лучше с многолетним опытом, и я узнал более вескую причину, чем просто сказать «поддерживать инкапсуляцию».
Определив методы получения и установки, вы обеспечите согласованный интерфейс, так что если вы захотите изменить свою реализацию, у вас будет меньше шансов нарушить зависимый код. Это особенно важно, когда ваши классы предоставляются через API и используются в других приложениях или третьими лицами. Так как насчет того, что входит в геттер или сеттер?
Получатели обычно лучше реализованы как простой скрытый проход для доступа к значению, потому что это делает их поведение предсказуемым. Я говорю вообще, потому что я видел случаи, когда геттеры использовались для доступа к значениям, управляемым вычислением или даже условным кодом. Как правило, не очень хорошо, если вы создаете визуальные компоненты для использования во время разработки, но, казалось бы, удобно во время выполнения. Однако нет реальной разницы между этим и использованием простого метода, за исключением того, что когда вы используете метод, вы, как правило, с большей вероятностью будете называть метод более подходящим образом, чтобы функциональность «получателя» была более очевидной при чтении кода.
Сравните следующее:
int aValue = MyClass.Value;
а также
int aValue = MyClass.CalculateValue();
Вторая опция проясняет, что значение вычисляется, тогда как первый пример говорит вам, что вы просто возвращаете значение, ничего не зная о самом значении.
Возможно, вы могли бы утверждать, что следующее будет более понятным:
int aValue = MyClass.CalculatedValue;
Проблема, однако, в том, что вы предполагаете, что значением уже манипулировали в другом месте. Таким образом, в случае получателя, хотя вы можете предположить, что при возврате значения может происходить что-то еще, трудно сделать такие вещи понятными в контексте свойства, и имена свойств никогда не должны содержать глаголов. в противном случае сложно сразу понять, следует ли при обращении использовать используемое имя в скобках.
Однако сеттеры - это немного другой случай. Вполне уместно, чтобы установщик обеспечивал некоторую дополнительную обработку для проверки данных, передаваемых в свойство, исключая исключение, если установка значения будет нарушать определенные границы свойства. Проблема, с которой сталкиваются некоторые разработчики при добавлении обработки к сеттерам, заключается в том, что всегда есть соблазн заставить сеттера сделать немного больше, например, выполнить вычисления или манипулировать данными каким-либо образом. Здесь вы можете получить побочные эффекты, которые в некоторых случаях могут быть непредсказуемыми или нежелательными.
В случае с сеттерами я всегда применяю простое эмпирическое правило, которое заключается в том, чтобы как можно меньше делать с данными. Например, я обычно разрешаю либо граничное тестирование, либо округление, чтобы я мог вызывать исключения, если это уместно, или избегать ненужных исключений, где их можно разумно избегать. Свойства с плавающей запятой являются хорошим примером, когда вы можете захотеть округлить чрезмерные десятичные разряды, чтобы избежать возникновения исключения, но при этом разрешить ввод значений диапазона с несколькими дополнительными десятичными разрядами.
Если вы применяете какие-то манипуляции с входом установщика, у вас возникает та же проблема, что и с геттером, из-за которой другим сложно понять, что делает установщик, просто назвав его. Например:
MyClass.Value = 12345;
Говорит ли это что-нибудь о том, что произойдет со значением, когда оно передается сеттеру?
Как насчет:
MyClass.RoundValueToNearestThousand(12345);
Второй пример точно сообщает вам, что произойдет с вашими данными, а первый не даст вам знать, будет ли ваше значение произвольно изменено. При чтении кода второй пример будет намного понятнее по назначению и функциям.
Прав ли я, что это полностью разрушило бы цель иметь в первую очередь геттеры и сеттеры, а валидация и другая логика (без странных побочных эффектов, конечно) должны быть разрешены?
Наличие методов получения и установки имеет значение не для инкапсуляции ради «чистоты», а для инкапсуляции, чтобы можно было легко реорганизовать код без риска изменения интерфейса класса, который в противном случае нарушил бы совместимость класса с вызывающим кодом. Валидация полностью уместна в установщике, однако существует небольшой риск того, что изменение в валидации может нарушить совместимость с вызывающим кодом, если вызывающий код зависит от валидации, происходящей определенным образом. Это, как правило, редкая ситуация с относительно низким уровнем риска, но ее следует отметить для полноты картины.
Когда должна произойти проверка?
Проверка должна происходить в контексте установщика до фактической установки значения. Это гарантирует, что в случае возникновения исключения состояние вашего объекта не изменится и потенциально лишит законной силы его данные. Я обычно считаю, что лучше делегировать валидацию отдельному методу, который будет первым, что вызывается в установщике, для того, чтобы код установщика был относительно беспорядочным.
Разрешено ли установщику изменять значение (возможно, преобразовать допустимое значение в какое-то каноническое внутреннее представление)?
В очень редких случаях, может быть. В общем, вероятно, лучше не делать этого. Такого рода вещи лучше оставить другому методу.