Представим себе ситуацию — есть некоторый класс, например Subscriber, у которого есть приватное поле string Subscriber::email и метод, устанавливающий его значение Subscriber::setEmail(string val).
Очевидно, что можно попытаться задать через этот сеттер некорректный адрес (например, не содержащий символ @).
Вопрос: 1. Должен ли этот сеттер проверять корректность мыла?
2. Если должен, то каким образом он должен сигнализировать о некорректном вводе?
Через возвращаемое значение или через исключение?
P.S. Прошу принять во внимание следующие доводы:
+каждый метод должен быть ответственен за одно действие, а в п.1 мы пытаемся возложить на этот сеттер и проверку значения и установку поля.
+если все же примериться с этим, и заставить метод возвращать false при попытке ввода некорректного мыла, то тогда придется каждый раз писать if (!s->setEmail(«blah-blah»)) {...} — как-то это слишком, мне кажется.
+исключение нужно генерировать тогда, когда ошибку нельзя исправить в том месте, где она была обнаружена. Обычно так бывает при большой вложенности стека вызовов функций, а в нашем случае исключение кажется не совсем уместным.
Проверять корректность должен.
При этом он остается ответственным за одно действие — за установку именно email'a, а не произвольной строки.
Поэтому и генерировать исключение при неправильных данных тоже должен.
Метод же неспроста называется setEmail.
Вы же не удивляетесь, что при попытке присвоить строку в integer поле получаете ошибку (вероятно даже компиляции).
Здесь же происходит фактически тоже самое.
Если говорить про идейность, то еще лучше сделать тип Email и проверки делать в нем. Но в обычном, а не идеальном коде, это почти наверняка будет излишним. Поэтому проверка в setter'e — хороший компромисс.
Все зависит от того, где этот сеттер доступен. Есть подход с разделением безопасных и небезопасных участков. Если у Вас в приложении есть UI и данные приходят из него, тогда код пользовательского интерфейса должен проверять корректность email, а сеттер должен устанавливать то, что ему дадут. Если же этот сеттер доступен пользователю или каким-то третьим лицам, тогда надо делать эту проверку внутри сеттера и тут уж надо смотреть на то, как реакция на ошибку обычно обрабатывается другими методами. Если везде исключения, пусть будут исключения, если везде возвращение False, то можно и так. Если же этот код только для разработчиков, то можно в дебаге делать assert, а в релизе кидать большое и страшное исключение (но только в случае, если некорректные данные приходят из-за кривой логики, пользовательский ввод должен проверяться задолго до этого).
Спасибо! Пользовательским вводом тут и не пахнет, пример приведен из реальной жизни, метод используется на большой глубине чуть ли не в самом сердце боевой системы. Но вопрос-то был о сеттерах вообще, так что Ваши разъяснения оказались очень кстати.
> каждый метод должен быть ответственен за одно действие
Не надо понимать все так буквально. Не стоит к примеру подключаться к базе, выбирать инфу, закачивать обратно результаты и сливать инфу в лог одной большой простыней в одной функции. Но это не значит, что каждую простейшую операцию (такую как присваивание) нужно выносить в отдельный метод.
> а в нашем случае исключение кажется не совсем уместным
Исключение говорит «я не могу выполнить команду», в данном случае не допускаем некорректный ввод. Не надо боятся и экономить на исключениях, это удобный механизм (вплоть до таких задач как остановить map(), но это уже вопрос вкуса). Не важно как глубоко «вложен» вызов до вашей функции/свойства, вы на самом деле этого не знаете и не можете знать.
Да, ваш сеттер должен проверять ввод и присваивать значение, если оно съедобно, иначе генерить исключение.
Опять же, не стоит слепо следовать одному правилу. Иногда для простоты допускается «некорректное» состояние объекта, для последующей более общей проверки. К примеру некоторая форма на странице или в окне пишет пользовательские изменения сразу в ваш объект (модель). Перед записью мы проверяем obj.validate(); если прошли проверку, сохранили изменения. Такой подход очень сильно упрощает форму и работу с вашей моделью (меньше танцев вокруг try/catch и временных значений вне модели).
Если объекту недопустимо пребывать в «некорректном» состоянии и вообще он immutable после создания, то создаем его через builder. Builder'у позволительно временно принимать некорректные значения, если само понятие корректности зависит от других свойств (к примеру e-mail принимаем только если флаг has_email установлен). Тогда валидация происходит в самом конце, когда строим финальный объект.
В обоих случаях выше не генерим исключение в setter'e. Все по задаче, без догм и бездумных рецептов.
Я так понимаю, у вас больше стоит вопрос когда и как проверять на валидность? Лично мне очень по душе концепция баррикад. Тем более если у вас это все глубоко в функционале. Просто фильтруйте входящие данные на ранних стадиях.
email здесь приведен в качестве примера, возможно и не самого удачного. Нельзя же в коде на каждый вид используемой информации заводить по отдельному классу. Так дело может дойти до того, что будем проверять у целочисленных отсутствие дробной части, заведя класс TrueInteger.
> Нельзя же в коде на каждый вид используемой информации заводить по отдельному классу.
Нельзя же теперь, в каждом классе, который будет использовать объект проводит свою собственную проверку.
Вот как раз сижу и думаю над этим вопросом =) С одной стороны некорректных данных на этом участке уже быть не должно. С другой стороны поставщика данных для этого сеттера пишу не я… А если будут исключения, то проще будет найти жучка, если вдруг начнут валиться некорректные данные.
Трудно судить о нюансах Вашей ситуации. Но когда каждый участник разработки будет отлавливать ошибки всех остальных — это уж точно не верный путь. Лучше не проверяйте, а логгируйте :)
С точки зрения зрения ООП, класс должен самостоятельно реализовывать логику реального объекта.