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

задняя часть
Почему ноль это плохо?
  • Оригинальный адрес:why null is bad
  • Автор оригинала: Егор Бугаенко
  • Перевод с: личный интерес (план перевода не Nuggets)
  • Переводчик: Обезьяна в Гао Лао Чжуан
  • Корректор: нет, перевод первый, читатели, пожалуйста, поправьте меня в комментариях

Давайте сначала рассмотрим простой пример использования 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, The Billion Dollar Mistakeи книга Дэвида Уэста «Объектное мышление». Затем я привожу в порядок все аргументы и использую подходящую объектно-ориентированную конструкцию вместо null в качестве возвращаемого значения. На данный момент кажется, что есть по крайней мере два способа использовать null вместо этого.

1. ИспользуйтеШаблон проектирования пустых объектов(желательно определить константу)

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Employee.NOBODY;
  }
  return Employee(id);
}

2. Когда объект не может быть возвращен, может быть выброшено исключение, чтобы сделать вызывающую программу отказоустойчивой (fail fast)

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    throw new EmployeeNotFoundException(name);
  }
  return Employee(id);
}

Теперь, глядя на аргументы против использования null, в дополнение к разговору Тони Хоареса и книге Дэвида Уэста, упомянутой выше, я также прочитал "Clean Code", Стив МакКоннеллCode Complete, Джон СонмезСкажи «Нет» «Нулю»"и обсуждение на StackOverflow"Is returning null bad design? ".

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

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

// 糟糕的设计,请勿复用
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}

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

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

null - это "феодальный пережиток" процедурного программирования, вместо этого используйте нулевые объекты или исключения.

семантическая неоднозначность

Чтобы явно выразить значение «функция вернет реальный объект или ноль», getByName() следует назвать getByNameOrNullIfNotFound(). Каждая подобная функция должна это делать, иначе это создаст двусмысленность для читателя кода.

Для семантической точности стоит определить более длинные имена для функций.

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

Некоторые люди будут утверждать, что иногда необходимо возвращать null для повышения производительности. Например, метод get() в интерфейсе карты java вернет значение null, если соответствующая запись не найдена на карте, например:

Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;

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

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

Очевидно, что этот способ в два раза медленнее первого, что делать? Я думаю, что дизайн интерфейса Map ошибочен (не в обиду автору), он должен возвращать итератор, чтобы наш код мог выглядеть так:

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

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

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

Если кто-то знает, что объект Java является указателем на некоторую структуру данных, и знает, что null является нулевым указателем (равным 0x00000000 в процессорах Intel x86), то он должен быть в состоянии принять оператор if(employee == null). Однако, если мыслить в терминах объектного мышления, это предложение бессмысленно. С точки зрения объекта наш код выглядит так:

  • Здравствуйте, это отдел программного обеспечения?
  • да.
  • Пожалуйста, позвольте мне поговорить с вашим сотрудником Джеффри.
  • Пожалуйста подождите...
  • Hello
  • ты НУЛЬ?

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

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

Медленный отказ

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

Убедитесь, что ваш метод имеет чрезвычайно высокие требования к операндам, предоставленным вызывающей стороной.Если данных, предоставленных вызывающей стороной, недостаточно или они вообще не соответствуют основному варианту использования метода, сгенерируйте исключение. Или верните нулевой объект, который демонстрирует некоторое общее поведение и создает исключение для всех других вызовов, как показано ниже:

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 ответственным за проблемы с производительностью вычислительной платформы, которые должны быть прозрачными для объектов Employee.

Вместо того, чтобы управлять состоянием и раскрывать поведение, связанное с бизнесом, позвольте объекту обрабатывать кэширование своих собственных результатов — для этого и нужна ленивая загрузка. Кэширование — это не то, чем сотрудники должны заниматься в офисе, не так ли?

Решение состоит в том, чтобы не использовать ленивую загрузку таким примитивным способом, как в примере выше. Вместо этого переместите эту проблему кэширования на другие уровни приложения. Например, в Java можно использовать методы аспектно-ориентированного программирования. Например, jcabi-aspects использует аннотацию @Cacheable для кэширования значений, возвращаемых методами:

import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}

Надеемся, что благодаря анализу этой статьи вы сможете отказаться от использования null в качестве возвращаемого значения в своем коде.