Статьи

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

Простой пример использования NULL в Java:

1
2
3
4
5
6
7
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}

Что не так с этим методом?

Он может вернуть NULL вместо объекта — вот что не так. NULL — ужасная практика в объектно-ориентированной парадигме, и ее следует избегать любой ценой. Уже было опубликовано несколько мнений об этом, в том числе « Пустые ссылки», презентация « Ошибка в миллиард долларов » Тони Хоара и вся книга « Думая об объекте » Дэвида Уэста.

Здесь я попытаюсь обобщить все аргументы и показать примеры того, как можно избежать использования NULL и заменить его на соответствующие объектно-ориентированные конструкции.

По сути, есть две возможные альтернативы NULL .

Первый — это шаблон проектирования Null Object (лучший способ сделать его постоянным):

1
2
3
4
5
6
7
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Employee.NOBODY;
  }
  return Employee(id);
}

Вторая возможная альтернатива — быстро потерпеть неудачу , выдав исключение, если вы не можете вернуть объект:

1
2
3
4
5
6
7
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    throw new EmployeeNotFoundException(name);
  }
  return Employee(id);
}

Теперь давайте посмотрим аргументы против NULL .

Помимо презентации Тони Хоара и книги Дэвида Уэста, упомянутой выше, я прочитал эти публикации перед тем, как написать этот пост: « Чистый код » Роберта Мартина, « Код завершен » Стивом Макконнеллом, скажите «Нет» «Нулевому» Джона Сонмеза, Возвращает ли ноль плохой дизайн? обсуждение в StackOverflow.

Специальная обработка ошибок

Каждый раз, когда вы получаете объект в качестве входных данных, вы должны проверить, является ли он NULL или действительной ссылкой на объект. Если вы забудете проверить, NullPointerException (NPE) может прервать выполнение во время выполнения. Таким образом, ваша логика загрязняется несколькими проверками, и если / then / else разветвляется:

1
2
3
4
5
6
7
8
// 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);
}

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

1
dept.getByName("Jeffrey").transferTo(dept2);

Представьте, что NULL ссылается на наследование процедурного программирования, и вместо этого используйте 1) Нулевые объекты или 2) Исключения.

Двусмысленная семантика

Чтобы явно передать его значение, функция getByName() должна иметь имя getByNameOrNullIfNotFound() . То же самое должно происходить с каждой функцией, которая возвращает объект или NULL . В противном случае двусмысленность неизбежна для читателя кода. Таким образом, чтобы сохранить семантическую однозначность, вы должны дать более длинные имена функциям.

Чтобы избавиться от этой неоднозначности, всегда возвращайте реальный объект, нулевой объект или генерируйте исключение.

Некоторые могут возразить, что нам иногда приходится возвращать NULL ради производительности. Например, метод get() интерфейса Map в Java возвращает NULL если на карте нет такого элемента:

1
2
3
4
5
Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;

Этот код ищет карту только один раз из-за использования NULL в Map . Если мы проведем рефакторинг Map чтобы его метод get() генерировал исключение, если ничего не найдено, наш код будет выглядеть так:

1
2
3
4
if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search

Очевидно, что этот метод в два раза медленнее, чем первый. Что делать?

Интерфейс Map (не в обиду его авторам) имеет недостаток дизайна. Его метод get() должен был возвращать Iterator чтобы наш код выглядел так:

1
2
3
4
5
Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();

Кстати, именно так и разработан метод C ++ STL map :: find () .

Мышление компьютера против мышления объекта

Оператор if (employee == null) понимается кем-то, кто знает, что объект в Java является указателем на структуру данных, а NULL — указателем на ничто ( 0x00000000 в процессорах Intel x86).

Однако, если вы начинаете думать как объект, это утверждение имеет гораздо меньше смысла. Вот как выглядит наш код с точки зрения объекта:

1
2
3
4
5
6
- Hello, is it a software department?
- Yes.
- Let me talk to your employee "Jeffrey" please.
- Hold the line please...
- Hello.
- Are you NULL?

Последний вопрос в этом разговоре звучит странно, не так ли?

Вместо этого, если они повесят трубку после того, как мы попросим поговорить с Джеффри, это станет для нас проблемой (исключение). В этот момент мы пытаемся снова позвонить или сообщить нашему руководителю, что мы не можем связаться с Джеффри и совершить более крупную транзакцию.

Кроме того, они могут позволить нам поговорить с другим человеком, который не является Джеффри, но который может помочь с большинством наших вопросов или отказать в помощи, если нам нужно что-то «специфичное для Джеффри» (Нулевой объект).

Медленная неудача

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

Этот аргумент близок к «специальной обработке ошибок», описанной выше.

Хорошей практикой является сделать ваш код как можно более хрупким, позволяя ему ломаться при необходимости.

Сделайте ваши методы чрезвычайно требовательными к данным, которыми они манипулируют. Позвольте им жаловаться, выдавая исключения, если предоставленные данные недостаточны или просто не соответствуют сценарию основного использования метода.

В противном случае, верните Null Object, который демонстрирует некоторое общее поведение и генерирует исключения для всех других вызовов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 используются при отложенной загрузке , чтобы сделать объекты неполными и изменчивыми. Например:

1
2
3
4
5
6
7
8
9
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-aspect имеет аннотацию @Cacheable которая кэширует значение, возвращаемое методом:

1
2
3
4
5
6
7
import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}

Я надеюсь, что этот анализ был достаточно убедителен, что вы перестанете делать код пустым!

Похожие сообщения

Вы также можете найти эти сообщения интересными:

Ссылка: Почему NULL это плохо? от нашего партнера JCG Егора Бугаенко в блоге « О программировании» .