Никогда не используйте NULL

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

Давайте сперва посмотрим общие сценарии использования, а затем обсудим, как можно улучшить код, чтобы сделать его более устойчивым к ошибкам и простым в использовании. Большинство отмеченных проблем становятся особенно заметными, когда ваши исходники использует кто-то ещё.

Для чего это используется

Примером частого сценария использования может быть внедрение дополнительных зависимостей через метод класса:

class SomeLoggingService {
    private $logger = null;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

 

В большинстве случаев logger будет установлен, но кто-то забудет об этом при использовании вашего сервиса. На сцену выходит второй разработчик и пишет новый метод в этом классе, используя свойство $logger. Во время разработки свойство всегда устанавливается и тестируется по соответствующим сценариям использования, так что разработчики забывают проверять на null – очевидно, это станет проблемой при других обстоятельствах. Вы полагаетесь на то, что методы будут вызваны в определённом порядке, который сложно документировать. Метод getLogger(), создающий нулевой логгер по умолчанию, мог бы решить эту проблему, но не гарантировано, поскольку второй разработчик может не знать об этом методе и просто использовать свойство.

В версиях PHP младше 7, вызов $this->logger->notice(…) приведёт к Fatal Error, ошибке, которая особенно плоха тем, что приложение не может обработать такой вид ошибок нормальным способом. В PHP 7 эти ошибки наконец-то стали отлавливаемыми, но, тем не менее – это не то, что вы ожидали бы в данной ситуации.

Ещё хуже отладка такого рода инициализации. К тому же это часто используется вместе с агрегированными объектами, которые требуются агрегирующимся классом. (Не следует использовать внедрение дополнительных зависимостей через метод класса для обязательных агрегатов, но пока рассматривается именно такой путь.). Давайте прямо сейчас рассмотрим следующий код:

class SomeService {
    public function someMethod() {
        $this->mandatoryAggregate->someOtherMethod(/* … */);
    }
}

 

При вызове метода someMethod() с не инициализированным свойством $mandatoryAggregate, мы получим фатальную ошибку, как было упомянуто выше. Даже если мы получим трассировку через XDebug или изменим код, чтобы бросить исключение, получив трассировку, нам всё ещё очень трудно будет понять, почему это свойство не инициализируется, так как создание SomeService обычно происходит за пределами текущего стека вызовов, но внутри Контейнера Внедрения Зависимостей (Dependency Injection Container) или во время инициализации приложения.

Разработчику при отладке остаётся только найти все места, где создаётся SomeService и проверять, инициализируется ли $mandatoryAggregate должным образом, и фиксить, если нет.

Решение

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

class SomeService {
    public function __construct(Aggregate $aggregate, Logger $logger = null) {
        $this->aggregate = $aggregate;
        $this->logger = $logger ?: new Logger\NullLogger();
    }
}

 

Параметр $aggregate теперь действительно обязателен, в то время как logger – дополнительный, но он всё равно будет всегда инициализирован. Logger\NullLogger может быть логгером, просто выбрасывающим все сообщения журнала прочь. Таким образом, нет больше потребности заботиться о проверке логгера каждый раз, когда вы хотите использовать его.

Используйте так называемый нулевой объект при необходимости в экземпляре по умолчанию, ничего не делающим. Другими примерами этого могут быть нулевая почтовая программа (не отправляющая письма) или нулевой кэш (не кэширующий). Такие нулевые объекты обычно тривиальны для реализации. На это стоит уделить время, поскольку вы обезопасите себя и выиграете много времени в конечном счёте, ибо не будете работать и отладывать Fatal Errors.

NULL – как возвращаемое значение

Аналогичная ситуация, когда null используется как возвращаемое значение для методов, документирующихся возвратом чего-то ещё. Такой подход всё ещё часто используется в состояниях ошибки вместо того, чтобы бросать исключения.

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

class BrokenInnerClass {
    public function innerMethod() {
        // …
        if ($error) {
            return null;
        }
        // …
    }
}

class DispatchingClass {
    public function dispatchingMethod() {
        return $this->brokenInnerClass->innerMethod();
    }
}

class MyUsingClass {
    public function myBeautifulMethod() {
        $value = $this->dispatchingClass->dispatchingMethod();
        $value->getSomeProperty(); // Fatal Error
    }
}

 

Обычно существуют ещё больше уровней абстракции. Мы живём в век фреймворков, в конце-то концов.

Решение

Если значение не может быть найдено, не возвращайте NULL, а бросайте исключение – есть даже встроенные исключения для таких случаев, например, OutOfBoundsException.

В стеке вызовов я смогу сразу увидеть, где именно перестало работать приложение. В оптимальном случае сообщение об исключении даже добавляет значение и даёт некоторые подсказки того, что мне нужно пофиксить.

Резюме

Использование NULL может быть допустимым как значения объектов, и когда вы просто хотите сказать, что ничего нет. Но в большинстве случаев NULL должен быть заменён бросанием исключения или обеспечением нулевого объекта, выполняющего API, но ничего не делающего. Эти нулевые объекты тривиальны и быстры для разработки. Возврат инвестиций будет огромным за счёт сохранённых часов отладки.

Примечание

Это авторский перевод статьи «Never Use null» на русский язык.

Комментарии

  1. Журналист пишет:

    По поводу DI, тут проблемы программиста, он закосячил при разработке и не убедился, что заинжектил то что надо. На каждую зависимость создавать Null объект, как-то это громоздко и не оправданно.

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

  2. Журналист пишет:

    По поводу возвращаемого Null. Только так и делают разработчики, не знаю где вы нашли пример кода с null вместо ошибки 🙂

  3. Artem Ostretsov пишет:

    Спасибо за перевод, но ссылку на оригинал лучше имхо вверху размещать 🙂

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *