JavaRush /Java блог /Random UA /Чому NULL – це погано?
Helga
26 рівень

Чому NULL – це погано?

Стаття з групи Random UA

Чому 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"); } } Сподіваюся, цей аналіз був досить переконливий, щоб ви припинабо обнулювати свій код :) Оригінал статті тут . Вам також можуть бути цікаві такі теми як: • DI Containers are Code PollutersGetters/Setters. Evil. Період. Anti-Patterns in OOPAvoid String ConcatenationObjects Should Be Immutable
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ