JavaRush /Java блог /Архив info.javarush /Почему NULL - это плохо?
Helga
26 уровень

Почему NULL - это плохо?

Статья из группы Архив info.javarush

Почему NULL – это плохо?

Вот простой пример использования NULL в Java: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return null; } return new Employee(id); } Что не так с этим методом? Он может вернуть NULL вместо объекта – вот что не так. Использование NULL – ужасная практика в ООП, и этого стоит избегать всеми способами. По данному вопросу уже опубликовано достаточно разных мнений, в том числе презентация Tony Hoare «Нулевые ссылки: Ошибка на миллиард долларов» и целая книга David West «Объектно ориентированное мышление». Здесь я попытаюсь суммировать все доводы и показать примеры того, как можно избежать использования NULL, заменив его подходящими объектно-ориентированными конструкциями. Сначала рассмотрим две возможные альтернативы NULL. Первая – это паттерн проектирования «Нулевой Объект» (лучше всего реализовывать его с помощью константы): public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return Employee.NOBODY; } return Employee(id); } Вторая возможная альтернатива – «быстрое поражение» через выбрасывание исключения в случае, если вернуть объект невозможно: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { throw new EmployeeNotFoundException(name); } return Employee(id); } А теперь давайте познакомимся с доводами против использования NULL Перед написанием этого поста я познакомился, кроме вышеупомянутых презентации Tony Hoare И книги David West, с целым рядом публикаций. Это «Чистый код» Robert Martin, «Совершенный код» Steve McConnell, «Скажите «Нет» NULL» John Sonmez и дискуссией на StackOverflow под названием «Возвращать NULL – плохая практика?»
Обработка ошибок вручную
Каждый раз, когда вы на входе получаете объект, вы должны проверять, является он ссылкой на действительный объект или NULL’ом. Если вы забудете проверить, ваша программа может быть прервана прямо во время исполнения выброшенным NullPointerExeption (NPE). Из-за этого ваш код начинает наполняться многочисленными проверками и разветвлениями if/then/else. // this is a terrible design, don't reuse Employee employee = dept.getByName("Jeffrey"); if (employee == null) { System.out.println("can't find an employee"); System.exit(-1); } else { employee.transferTo(dept2); } Именно так исключительные ситуации должны обрабатываться в С и других строго процедурных языках программирования. В ООП обработка исключений введена главным образом как раз для того, чтобы избавиться от вручную написанных блоков обработки. В ООП мы позволяем исключениям всплывать пока они не достигнут обработчика ошибок всего приложения, и благодаря этому наш код становится намного чище и короче: dept.getByName("Jeffrey").transferTo(dept2); Считайте NULL ссылки пережитками процедурного стиля программирования и используйте 1) Нулевые Объекты или 2) Исключения вместо них.
Неоднозначное понимание
Чтобы точно передать в названии смысл происходящего, метод getByName() должен быть переименован в getByNameOrNullIfNotFound(). То же самое нужно сделать для каждого метода, который возвращает объект или NULL, иначе при чтении кода не избежать неоднозначности. Таким образом, для того, чтобы названия методов были точны, вы должны давать методам более длинные имена. Чтобы избежать неоднозначности всегда возвращайте реальный объект, нулевой объект или выбрасывайте исключение. Кто-то может возразить, что иногда нам просто необходимо возвратить NULL чтобы добиться нужного результата. Например, метод get() интерфейса Map в Java возвращает NULL, когда в Map нет больше объектов. Employee employee = employees.get("Jeffrey"); if (employee == null) { throw new EmployeeNotFoundException(); } return employee; Благодаря использованию NULL в Map этому коду хватает всего одного цикла поиска для получения результата. Если мы перепишем Map таким образом, чтобы метод get() выбрасывал исключение в случае, если ничего не найдено, наш код будет выглядеть так: if (!employees.containsKey("Jeffrey")) { // first search throw new EmployeeNotFoundException(); } return employees.get("Jeffrey"); // second search Очевидно, что этот метод в два раза медленнее, чем исходный. Что же делать? В интерфейсе Map (без намерения обидеть разработчиков) есть недостаток проектирования. Его метод get() должен был бы возвращать Iterator, и тогда наш код выглядел бы так: Iterator found = Map.search("Jeffrey"); if (!found.hasNext()) { throw new EmployeeNotFoundException(); } return found.next(); Кстати, именно так спроектирован метод STL map::find() в С++.
Компьютерное мышление против объектно-ориентированного
Строка кода if (employee == null) вполне понятна тому, кто знает, что объект в Java – это указатель на структуру данных, а NULL – это указатель на ничто (в процессорах Intel x86 – 0x00000000). Однако если вы начнете мыслить в объектном стиле, эта строка становится намного менее осмысленной. Вот как наш код выглядит с объектной точки зрения:
- Здравствуйте, это отдел разработки ПО? - Да. - Будьте добры, пригласите к телефону вашего сотрудника Джефри. - Подождите минутку... - Здравствуйте. - Вы NULL?
Последний вопрос звучит немного странно, не так ли? Если вместо этого после вашей просьбы пригласить к телефону Джефри на том конце просто повесят трубку, это вызовет для нас определенные сложности (Исключение). В этом случае мы можем попробовать перезвонить или же доложим нашему начальнику о том, что мы не смогли поговорить с Джефри, и завершим свою основную задачу. Кроме этого, на той стороне вам могут предложить поговорить с другим человеком, который, хоть и не является Джефри, может либо помочь вам с большинством ваших вопросов, либо отказаться помогать, если нам нужно узнать что-то, что знает только Джефри (Нулевой Объект).
Медленный провал
Вместо быстрого завершения работы, код выше пытается умереть медленно, убивая других на своем пути. Вместо того, чтобы дать всем понять, что что-то пошло не так и нужно немедленно начинать обработку исключительного события, он пытается скрыть свой провал от клиента. Это очень похоже на ручную обработку исключений, о которой мы говорили выше. Делать свой код как можно более хрупким и позволять ему прерываться, если это нужно – хорошая практика. Делайте свои методы предельно требовательными к данным, с которыми они работают. Позволяйте им жаловаться, выкидывая исключения, если данных, которые им предоставили, недостаточно, или же данные просто не подходят для использования в этом методе по задуманному сценарию. В противном случае возвращайте Нулевой Объект, который ведет себя каким-то общепринятым способоы и выбрасывает исключения во всех других случаях. public Employee getByName(String name) { int id = database.find(name); Employee employee; if (id == 0) { employee = new Employee() { @Override public String name() { return "anonymous"; } @Override public void transferTo(Department dept) { throw new AnonymousEmployeeException( "I can't be transferred, I'm anonymous" ); } }; } else { employee = Employee(id); } return employee; }
Изменяемые и незавершенные объекты
Вообще, строго рекомендуется проектировать объекты так, чтобы они были неизменяемыми. Это значит, объект должен получить все необходимые данные при его создании и никогда не менять своего состояния в течение всего жизненного цикла. Значения NULL очень часто используются в паттерне проектирования «Ленивая загрузка» для того, чтобы сделать объекты незавершенными и изменяемыми. Пример: public class Department { private Employee found = null; public synchronized Employee manager() { if (this.found == null) { this.found = new Employee("Jeffrey"); } return this.found; } } Несмотря на то, что эта технология широко распространена, для ООП она является антипаттерном. И главным образом потому, что заставляет объект нести ответственность за проблемы с производительностью у вычислительной платформы, а это как раз то, о чем объект Employee не может быть осведомлен. Вместо того, чтобы управлять своим состоянием и вести себя соответствующим своему предназначению образом, объект вынужден заботиться о кэшировании своих собственных результатов – вот к чему приводит «ленивая загрузка». А ведь кэширование – это вовсе не то, чем занимается сотрудник в офисе, не так ли? Выход? Не используйте «ленивую загрузку» таким примитивным способом, как в вышеприведенном примере. Вместо этого переместите кэширование проблем на другой уровень своего приложения. Например, в Java вы можете использовать возможности аспектно-ориентированного программирования. Например, в jcabi-aspects есть аннотация @Cacheable, которая кэширует значение, возвращаемое методом. import com.jcabi.aspects.Cacheable; public class Department { @Cacheable(forever = true) public Employee manager() { return new Employee("Jacky Brown"); } } Надеюсь, этот анализ был достаточно убедителен, чтобы вы прекратили обNULLять свой код :) Оригинал статьи здесь. Вам также могут быть интересны такие темы как: • DI Containers are Code PollutersGetters/Setters. Evil. Period.Anti-Patterns in OOPAvoid String ConcatenationObjects Should Be Immutable
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
new-Object Уровень 30
2 марта 2015
спасибо, было интересно.
terranum Уровень 28
28 февраля 2015
Нужная тема. Спасибо!