The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
Простой пример использования NULL
в Java:
Что не так с этим методом?
Он может вернуть NULL
вместо объекта — вот что не так. Использование NULL
является ужасной практикой в объектно-ориентированной парадигме и должно быть избегано во всех случаях. Уже было опубликовано множество мнений об этом, включая презентацию Null References, The Billion Dollar Mistake Тони Хоара и всю книгу Object Thinking Дэвида Уэста.
Здесь я постараюсь подвести итоги всех аргументов и показать примеры того, как можно избежать использования NULL
и заменить его правильными объектно-ориентированными конструкциями.
В основном, существует два возможных альтернативных варианта для NULL
.
Первый - это паттерн проектирования Null Object (лучший способ - сделать его константой):
Второй возможный вариант - это быстро завершить выполнение, сгенерировав исключение, когда невозможно вернуть объект.
Теперь давайте рассмотрим аргументы против NULL
.
Помимо презентации Тони Хоара и книги Дэвида Уэста, упомянутых выше, я прочитал следующие публикации перед написанием этого поста: Чистый код Роберта Мартина, Полное кодирование Стива Макконнелла, Говорим “нет” “null” Джона Сонмеза, Возвращение null - плохой дизайн? обсуждение на Stack Overflow.
Каждый раз, когда вы получаете объект в качестве входных данных, вы должны проверить, является ли он NULL
или действительной ссылкой на объект. Если вы забудете проверить, NullPointerException
(NPE) может прервать выполнение во время выполнения. Таким образом, ваша логика становится загрязненной множественными проверками и ветвлениями if/then/else.
Вот как предполагается обрабатывать исключительные ситуации в Си и других императивных процедурных языках. ООП ввело обработку исключений в первую очередь, чтобы избавиться от этих импровизированных блоков обработки ошибок. В ООП мы позволяем исключениям всплывать, пока они не достигнут обработчика ошибок на уровне приложения, и наш код становится гораздо более чистым и коротким.
Рассмотрите ссылки NULL
как наследие процедурного программирования и вместо этого используйте 1) объекты Null или 2) исключения.
Для ясного выражения своего значения функция getByName()
должна называться getByNameOrNullIfNotFound()
. То же самое должно происходить с каждой функцией, возвращающей объект или NULL
. В противном случае неизбежна неоднозначность для читателя кода. Таким образом, для того чтобы семантика была ясной, следует давать функциям более длинные имена.
Чтобы избавиться от этой неоднозначности, всегда возвращайте реальный объект, пустой объект или генерируйте исключение.
Некоторые могут возразить, что иногда нам нужно возвращать NULL
ради производительности. Например, метод get()
интерфейса Map
в Java возвращает NULL
, когда такого элемента нет в карте:
Этот код выполняет поиск по карте только один раз благодаря использованию NULL
в Map
. Если мы перестроим Map
, чтобы его метод get()
выбрасывал исключение, если ничего не найдено, наш код будет выглядеть так:
Очевидно, что этот метод в два раза медленнее первого. Что делать?
Интерфейс Map
(со всем уважением к его авторам) имеет конструктивный недостаток. В его методе get()
должен был возвращаться Iterator
, чтобы наш код выглядел так:
Кстати, именно так разработан метод map::find() в C++ STL.
Утверждение if (employee == null)
понятно для тех, кто знает, что объект в Java является указателем на структуру данных, а NULL
- указатель на ничто (0x00000000
в процессорах Intel x86).
Однако, если начать мыслить как объект, это утверждение имеет гораздо меньше смысла. Вот как наш код выглядит с точки зрения объекта:
Последний вопрос в этом разговоре звучит странно, не так ли?
Вместо этого, если они повесили трубку после нашей просьбы поговорить с Джеффри, это вызывает проблему для нас (Исключение). В этот момент мы пытаемся позвонить снова или сообщаем нашему руководителю, что мы не можем связаться с Джеффри и завершить более крупную сделку.
Вариантом является то, что они могут позволить нам поговорить с другим человеком, который не является Джеффри, но может помочь с большинством наших вопросов или отказаться помочь, если нам нужно что-то «специфичное для Джеффри» (Пустой объект).
Вместо быстрого выхода из ситуации при ошибке код выше пытается умирать медленно, унося с собой других. Вместо того, чтобы сообщить всем, что что-то пошло не так и что обработка исключений должна начаться немедленно, он скрывает эту ошибку от своего клиента.
Этот аргумент близок к “ад-хок обработке ошибок”, о которой речь шла выше.
Хорошая практика - сделать код таким хрупким, насколько это возможно, позволить ему ломаться, когда это необходимо.
Сделайте ваши методы крайне требовательными к данным, с которыми они работают. Позвольте им возмущаться и выбрасывать исключения, если предоставленные данные недостаточны или просто не соответствуют основному сценарию использования метода.
В противном случае, верните объект Null, который обладает некоторым общим поведением и выбрасывает исключения при всех остальных вызовах:
Say, you are designing a method findUserByName(), which has to find a user in the database. What would you return if nothing is found? #elegantobjects
--- Yegor Bugayenko (@yegor256) April 29, 2018
Mutable and Incomplete Objects
В целом, настоятельно рекомендуется проектировать объекты с учетом неизменяемости. Это означает, что объект получает всю необходимую информацию при его создании и не изменяет свое состояние на протяжении всего жизненного цикла.
Очень часто применяются значения NULL
при ленивой загрузке, чтобы сделать объекты неполными и изменяемыми. Например:
Эта технология, хотя и широко используется, является анти-паттерном в ООП. В основном потому, что она делает объект ответственным за проблемы производительности вычислительной платформы, о которых объект “Сотрудник” не должен знать.
Вместо управления состоянием и предоставления своего делового поведения объект должен заботиться о кэшировании собственных результатов - в этом суть ленивой загрузки.
Кэширование - это не то, чем занимается сотрудник в офисе, правда?
Решение? Не используйте ленивую загрузку таким примитивным способом, как в приведенном выше примере. Вместо этого переместите эту проблему кэширования на другой уровень вашего приложения.
Например, на языке Java вы можете использовать аспектно-ориентированное программирование. Например, jcabi-aspects имеет аннотацию @Cacheable
, которая кэширует значение, возвращаемое методом:
Я надеюсь, что этот анализ был достаточно убедительным, чтобы вы перестали использовать NULL
в своем коде.
п.с. Если вас интересует более научный аргумент против использования NULL, прочитайте эту недавно опубликованную исследовательскую статью, которая эмпирически демонстрирует корреляцию между интенсивностью использования NULL и когнитивной сложностью - чем больше NULL встречается в классе, тем выше сложность его методов.
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-15 at 07:14