Статьи

Геттеры и сеттеры считаются вредными

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

JavaBeans

Методы получения и установки возникли в спецификации JavaBeans, которая вышла первоначально в конце 1996 года, и была обновлена ​​до версии 1.01 в августе 1997 года. Первоначальная идея состояла в том, чтобы позволить создавать объекты, которые можно использовать как строительные блоки для составления приложений. Идея состояла в том, чтобы «пользователь» мог использовать какой-то инструмент-конструктор для соединения и настройки набора компонентов JavaBeans для совместной работы в качестве приложения. Например, кнопка в приложении AWT будет bean-компонентом (AWT был предшественником библиотеки пользовательского интерфейса Java Swing). В качестве альтернативы, некоторые JavaBean-компоненты могут быть больше похожи на обычные приложения, которые затем могут быть объединены в составные документы, поэтому бин электронной таблицы может быть встроен в веб-страницу.

Объект является JavaBean, если он придерживается следующих соглашений:

  1. Он должен иметь конструктор с нулевым аргументом, который не может завершиться ошибкой.
  2. У него есть свойства, которые доступны и видоизменяются с помощью методов «getter» и «setter».
  3. Для любого свойства компонента с именем Foo метод доступа должен называться getFoo . В случае логических свойств геттер может альтернативно называться isFoo .
  4. Метод установки для Foo должен называться setFoo .
  5. Бин не обязан представлять как метод получения, так и метод установки для каждого свойства: свойство с методом получения и без метода установки доступно только для чтения; свойство с установщиком и без получателя доступно только для записи.

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

Метафора неверна

Понятие «получить» и «установить» кажется достаточно естественным, но верно ли это? Соглашение JavaBeans использует «get» для обозначения запроса, что является операцией без побочных эффектов, но в реальном мире получение — это действие, которое изменяет состояние: если я получу книгу с полки, книга больше не будет на полке , Вы можете возразить, что это просто педантизм, но я думаю, что это заблуждение побуждает нас неправильно думать о том, как мы пишем наши объекты для взаимодействия друг с другом. Например, если бы у нас был класс Thermometer, то большинство разработчиков Java написали бы код для чтения температуры следующим образом:

1
Temperature t = thermometer.getTemperature();

Правда, это работа термометра, чтобы «получить» температуру? Нет! Работа термометра заключается в измерении температуры. Почему я работаю над этим пунктом? Это потому, что «получить» является обязательным утверждением: это инструкция для термометра, чтобы сделать что-то. Но мы не хотим давать указание термометру что-либо здесь делать; он уже выполняет свою работу (измеряет температуру), и мы просто хотим знать, каковы его текущие показания. Акт чтения сделан нами. Поэтому код более естественен, если он написан так:

1
Temperature t = thermometer.reading();

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

Объекты не являются структурами данных

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

Если наши привычки в кодировании склоняют нас к тому, чтобы рассматривать объекты как простые структуры данных, платформы ORM положительно применяют это. Что еще хуже, если вы используете Spring Framework — и если вы Java-разработчик, у вас есть хорошие шансы — по умолчанию он создает все ваши компоненты как синглтоны. (Смущает, что бины Spring не имеют ничего общего с JavaBeans). Итак, теперь у вас есть система, состоящая из одноэлементных объектов, работающих на структурах данных без какого-либо поведения. Если хранение вашего кода и данных по отдельности звучит как знакомый вам стиль программирования, вы не ошибаетесь: мы называем это процедурным программированием.

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

Что делать вместо

По возможности, перестань писать, получи и установи! Иногда это будет уместно, но определенно прекратите использовать возможности вашей IDE для генерации методов получения и установки для вас. Это просто удобный способ быстро сделать не то, что нужно. Если вам нужно выставить атрибут объекта, просто назовите его после атрибута, но также проверьте, действительно ли необходимо выставить его. Спросите, почему вы хотите это сделать. Можно ли делегировать задачу самому объекту? Например, скажем, у меня есть класс, представляющий сумму в валюте, и я хочу суммировать несколько транзакций:

1
2
3
4
Amount total = new Amount(transactions.stream()
        .map(Transaction::getAmount)
        .mapToDouble(Amount::getValue)
        .sum());

Вместо getValue почему бы не дать классу Amount метод add() и сделать для него суммирование?

1
2
3
Amount total = transactions.stream()
        .map(Transaction::getAmount)
        .reduce(Amount.ZERO, Amount::add);

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

Возможно, вы хотите получить данные объекта, чтобы проверить, равно ли оно чему-либо. В этом случае рассмотрите возможность реализации метода equals() для объекта и проверьте его на равенство. Если вы используете Mockito для создания шпионов, это избавляет от необходимости использовать захватчики аргументов: вместо этого вы можете создать объект с одинаковым значением в качестве примера и передать его непосредственно в оператор verify для сравнения.

Будут времена, когда вы должны создать аксессоры. Например, чтобы сохранить данные в базе данных, вам может потребоваться доступ к примитивным представлениям ваших данных. Вы действительно должны следовать соглашению об именовании get / set? Если ваш ответ «это так, как это делается в Java», я призываю вас вернуться и прочитать спецификацию JavaBeans. Вы действительно пишете JavaBean, чтобы использовать его так, как описано в спецификации? Используете ли вы фреймворк или библиотеку, которая ожидает, что ваши объекты будут следовать соглашению?

Там будет меньше раз, когда вы должны создавать мутаторы. Функциональное программирование в настоящий момент охватывает сумасшедшую индустрию, и принцип неизменяемых данных хорош. Это должно быть применено и к ОО-программам. Если нет необходимости изменять состояние, вы должны считать необходимым не изменять состояние, поэтому не добавляйте метод мутатора. Когда вы пишете код, который приводит к новому состоянию, везде, где возможно, возвращайте новые экземпляры для представления нового состояния. Например, арифметические методы в экземпляре BigDecimal не изменяют свое собственное значение: они возвращают новые экземпляры BigDecimal, представляющие их результаты. В настоящее время у нас достаточно памяти и вычислительной мощности, чтобы программирование стало возможным. А среда Spring не требует методов установки для внедрения зависимостей, она также может внедряться через аргументы конструктора. Этот подход действительно так, как рекомендует документация Spring .

Некоторые технологии требуют, чтобы классы следовали соглашению JavaBeans. Если вы все еще пишете страницы JSP для своего слоя представления, EL и JSTL ожидают, что объекты модели ответа будут иметь методы получения. Библиотеки для сериализации / десериализации объектов в и из XML могут потребовать этого. Платформы ORM могут требовать этого. Когда вы вынуждены писать структуры данных по этим причинам, я рекомендую держать их скрытыми за архитектурными границами. Не позволяйте этим структурам данных маскироваться, когда объекты попадают в ваш домен.

Вывод

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

Опубликовано на Java Code Geeks с разрешения Ричарда Уайлда, партнера нашей программы JCG . Смотрите оригинальную статью здесь: добытчики и сеттеры считаются вредными

Мнения, высказанные участниками Java Code Geeks, являются их собственными.