JavaRush /Blog Java /Random-PL /Dlaczego wartość NULL jest zła?
Helga
Poziom 26

Dlaczego wartość NULL jest zła?

Opublikowano w grupie Random-PL

Dlaczego wartość NULL jest zła?

Oto prosty przykład użycia wartości NULL w Javie: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return null; } return new Employee(id); } Co jest nie tak z tą metodą? Może zwrócić NULL zamiast obiektu - to jest błąd. Używanie NULL jest okropną praktyką w OOP i należy go za wszelką cenę unikać. Na ten temat opublikowano już całkiem sporo różnych opinii, w tym prezentację Tony’ego Hoare’a „Zero Links: A Billion Dollar Mistake” i całą książkę Davida Westa „Object-Oriented Thinking”. Tutaj postaram się podsumować wszystkie argumenty i pokazać przykłady, jak można uniknąć używania NULL, zastępując go odpowiednimi konstrukcjami zorientowanymi obiektowo. Najpierw przyjrzyjmy się dwóm możliwym alternatywom dla NULL. Pierwszym z nich jest wzorzec projektowy obiektu zerowego (najlepiej zaimplementowany ze stałą): public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return Employee.NOBODY; } return Employee(id); } Drugą możliwą alternatywą jest „szybkie niepowodzenie” poprzez zgłoszenie wyjątku, jeśli obiekt nie może zostać zwrócony: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { throw new EmployeeNotFoundException(name); } return Employee(id); } Teraz przyjrzyjmy się argumentom przemawiającym przeciwko użyciu wartości NULL. Przed napisaniem tego Podczas postu, oprócz wspomnianych wyżej prezentacji Tony’ego Hoare’a i książki Davida Westa, zapoznałem się z szeregiem publikacji. Są to „Czysty kod” Roberta Martina, „Czysty kod” Steve’a McConnella, „Powiedz nie NULL” Johna Sonmeza oraz dyskusja na temat StackOverflow zatytułowana „Czy zwracanie wartości NULL jest złą praktyką?”
Ręczna obsługa błędów
Za każdym razem, gdy jako dane wejściowe otrzymujesz obiekt, musisz sprawdzić, czy jest to odniesienie do obiektu rzeczywistego, czy wartość NULL. Jeśli zapomnisz to sprawdzić, wykonywanie programu może zostać przerwane w połowie wykonania przez zgłoszony wyjątek NullPointerExeption (NPE). Z tego powodu Twój kod zaczyna się wypełniać licznymi czekami i rozgałęzieniami 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); } W ten sposób należy obsługiwać wyjątki w C i innych ściśle proceduralnych językach programowania. W OOP wprowadzono obsługę wyjątków głównie po to, aby pozbyć się ręcznie pisanych bloków przetwarzania. W OOP pozwalamy, aby wyjątki rosły, dopóki nie dotrą do procedury obsługi błędów obejmującej całą aplikację, a to sprawia, że ​​nasz kod jest znacznie czystszy i krótszy: dept.getByName("Jeffrey").transferTo(dept2); Weź pod uwagę odniesienia NULL jako relikt stylu programowania proceduralnego i zamiast tego użyj 1) obiektów zerowych lub 2) wyjątków .
Niejednoznaczne zrozumienie
Coбы точно передать в названии смысл происходящего, метод getByName() должен быть переименован в getByNameOrNullIfNotFound(). То же самое нужно сделать для каждого метода, который возвращает obiekt Lub NULL, иначе при чтении kodа не избежать неоднозначности. Таким образом, для того, чтобы названия методов были точны, вы должны давать методам более длинные имена. Coбы избежать неоднозначности всегда возвращайте реальный obiekt, нулевой obiekt Lub выбрасывайте исключение. Кто-то может возразить, что иногда нам просто необходимо возвратить NULL чтобы добиться нужного результата. Например, метод get() интерфейса Map в Java возвращает NULL, когда в Map нет больше obiektов. Employee employee = employees.get("Jeffrey"); if (employee == null) { throw new EmployeeNotFoundException(); } return employee; Благодаря использованию NULL в Map этому kodу хватает всего одного цикла поиска для получения результата. Если мы перепишем Map таким образом, чтобы метод get() выбрасывал исключение в случае, если ничего не найдено, наш kod будет выглядеть так: if (!employees.containsKey("Jeffrey")) { // first search throw new EmployeeNotFoundException(); } return employees.get("Jeffrey"); // second search Очевидно, что этот метод в два раза медленнее, чем исходный. Co же делать? В интерфейсе Map (без намерения обидеть разработчиков) есть недостаток проектирования. Его метод get() должен был бы возвращать Iterator, и тогда наш kod выглядел бы так: Iterator found = Map.search("Jeffrey"); if (!found.hasNext()) { throw new EmployeeNotFoundException(); } return found.next(); Кстати, именно так спроектирован метод STL map::find() в С++.
Компьютерное мышление против obiektно-ориентированного
Строка kodа if (employee == null) вполне понятна тому, кто знает, что obiekt в Java – это указатель на структуру данных, а NULL – это указатель на ничто (в процессорах Intel x86 – 0x00000000). Однако если вы начнете мыслить в obiektном стиле, эта строка становится намного менее осмысленной. Вот Jak наш kod выглядит с obiektной точки зрения:
- Здравствуйте, это отдел разработки ПО? - Да. - Будьте добры, пригласите к телефону вашего сотрудника Джефри. - Подождите minutyку... - Здравствуйте. - Вы NULL?
Последний вопрос звучит немного странно, не так ли? Если zamiast этого после вашей просьбы пригласить к телефону Джефри на том конце просто повесят трубку, это вызовет для нас определенные сложности (Исключение). В этом случае мы можем попробовать перезвонить Lub же доложим нашему начальнику о том, что мы не смогли поговорить с Джефри, и завершим свою основную задачу. Кроме этого, на той стороне вам могут предложить поговорить с другим человеком, который, хоть и не является Джефри, может либо помочь вам с большинством ваших вопросов, либо отказаться помогать, если нам нужно узнать что-то, что знает только Джефри (Нулевой Объект).
Медленный провал
Вместо быстрого завершения работы, kod выше пытается умереть медленно, убивая других на своем пути. Вместо того, чтобы дать всем понять, что что-то пошло не так и нужно немедленно начинать обработку исключительного события, он пытается скрыть свой провал от клиента. Это очень похоже на ручную обработку исключений, о которой мы говорLub выше. Делать свой kod Jak можно более хрупким и позволять ему прерываться, если это нужно – хорошая практика. Делайте свои методы предельно требовательными к данным, с которыми они работают. Позволяйте им жаловаться, выкидывая исключения, если данных, которые им предоставLub, недостаточно, Lub же данные просто не подходят для использования в этом методе по задуманному сценарию. В противном случае возвращайте Нулевой Объект, который ведет себя Jakим-то общепринятым способоы и выбрасывает исключения во всех других случаях. 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; }
Изменяемые и незавершенные obiektы
Вообще, строго рекомендуется проектировать obiektы так, чтобы они были неизменяемыми. Это значит, obiekt должен получить все необходимые данные при его создании и никогда не менять своего состояния в течение всего жизненного цикла. Значения NULL очень часто используются в паттерне проектирования «Ленивая загрузка» для того, чтобы сделать obiektы незавершенными и изменяемыми. Пример: public class Department { private Employee found = null; public synchronized Employee manager() { if (this.found == null) { this.found = new Employee("Jeffrey"); } return this.found; } } Несмотря на то, что эта технология широко распространена, для ООП она является антипаттерном. И главным образом потому, что заставляет obiekt нести ответственность за проблемы с производительностью у вычислительной платформы, а это Jak раз то, о чем obiekt Employee не может быть осведомлен. Вместо того, чтобы управлять своим состоянием и вести себя соответствующим своему предназначению образом, obiekt вынужден заботиться о кэшировании своих собственных результатов – вот к чему приводит «ленивая загрузка». А ведь кэширование – это вовсе не то, чем занимается сотрудник в офисе, не так ли? Выход? Не используйте «ленивую загрузку» таким примитивным способом, Jak в вышеприведенном примере. Вместо этого переместите кэширование проблем на другой уровень своего Aplikacje. Например, в Java вы можете использовать возможности аспектно-ориентированного программирования. Например, в jcabi-aspects есть adnotacja @Cacheable, которая кэширует oznaczający, возвращаемое методом. import com.jcabi.aspects.Cacheable; public class Department { @Cacheable(forever = true) public Employee manager() { return new Employee("Jacky Brown"); } } Надеюсь, этот анализ был достаточно убедителен, чтобы вы прекратLub обNULLять свой kod :) Оригинал статьи здесь. Вам также могут быть интересны такие темы Jak: • DI Containers are Code PollutersGetters/Setters. Evil. Period.Anti-Patterns in OOPAvoid String ConcatenationObjects Should Be Immutable
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION