понедельник, 22 мая 2017 г.

Почему значение NULL это плохо?

Простой пример использования NULL в Java:
Public Employee getByName (String name) {
  Int id = database.find (name);
  If (id == 0) {
    Return null;
  }
  Return new Employee (id);
}
Что не так с этим методом?

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

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

В принципе, возможны две альтернативы NULL.

Первый - шаблон проектирования Null Object (лучше всего сделать его константой):
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.

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


Временная обработка ошибок

Каждый раз, когда вы получаете объект как входной, вы должны проверить, является ли он NULL или действительной ссылкой на объект. Если вы забыли проверить, исключение NullPointerException (NPE) может нарушить выполнение во время выполнения. Таким образом, ваша логика становится загрязненной несколькими проверками, а также ветвлением if / then / else:
// это ужасный дизайн, не используйте его
Employee employee = dept.getByName ("Jeffrey");
If (employee == null) {
  System.out.println («не удается найти сотрудника»);
  System.exit (-1);
} Else {
  employee.transferTo (dept2);
}
Именно так должны рассматриваться исключительные ситуации на C и других императивных процедурных языках. ООП вводит обработку исключений, в первую очередь, чтобы избавиться от этих временных блоков обработки ошибок. В ООП мы допускаем всплывание исключений до тех пор, пока они не достигнут обработчика ошибок для всего приложения, и наш код становится намного чище и короче:
dept.getByName("Jeffrey").transferTo(dept2);
Рассмотрим ссылки NULL на наследование процедурного программирования и используйте вместо него 1) Нулевые объекты или 2) Исключения.
 
Неоднозначная семантика

Чтобы явно передать смысл, функция getByName () должна быть названа getByNameOrNullIfNotFound (). То же самое должно произойти с каждой функцией, возвращающей объект или NULL. В противном случае, двусмысленность неизбежна для считывателя кода. Таким образом, чтобы сохранить семантическую однозначность, вы должны давать более длинные имена для функций.
Чтобы избавиться от двусмысленности, всегда возвращайте реальный объект, нулевой объект или бросайте исключение.
Некоторые могут утверждать, что иногда нам приходится возвращать NULL, ради производительности. Например, метод get () интерфейса Map в Java возвращает NULL, если в коллекции нет такого элемента:
Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;
Если мы реорганизуем Map так, чтобы его метод get () выдавал исключение, если ничего не найдено, наш код будет выглядеть так:
if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search
Очевидно, что этот метод в два раза медленнее первого. Что делать?

Интерфейс Map (без обид для его авторов) имеет недостатки дизайна. Его метод get () должен был возвращать итератор, чтобы наш код выглядел так:
Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();
Кстати, именно так и устроен метод map: find () C ++ STL.

Компьютерное мышление и объектное мышление

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

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

- Привет, это отдел программного обеспечения?
- Да.
- Позвольте мне поговорить с вашим сотрудником «Джеффри», пожалуйста.
- Пожалуйста, не кладите трубку...
- Здравствуйте.
- Вы Null?

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

В качестве альтернативы, они могут позволить нам поговорить с другим человеком, который не Джеффри, но который может помочь с большинством наших вопросов или отказаться от помощи, если нам потребуется что-то «специфическое для Джеффри» (объект 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-aspect имеет @Cacheable аннотацию, которая кэширует значение, возвращаемое методом:
import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}
Надеюсь, этот анализ был достаточно убедителен в том, что вы прекратите использовать NULL в вашем коде :)

Комментариев нет:

Отправить комментарий