«1. Обзор

В этом кратком руководстве мы рассмотрим паттерн Null Object, частный случай паттерна стратегии. Мы опишем его цель и когда мы действительно должны рассмотреть его использование.

Как обычно, мы также приведем простой пример кода.

2. Шаблон нулевого объекта

В большинстве объектно-ориентированных языков программирования нам не разрешено использовать нулевую ссылку. Вот почему нам часто приходится писать проверки на null:

Command cmd = getCommand();
if (cmd != null) {
    cmd.execute();
}

Иногда, если количество таких операторов if становится большим, код может стать уродливым, трудным для чтения и подверженным ошибкам. Вот когда может пригодиться шаблон Null Object.

Цель шаблона Null Object состоит в том, чтобы свести к минимуму подобную проверку на null. Вместо этого мы можем идентифицировать нулевое поведение и инкапсулировать его в тип, ожидаемый клиентским кодом. Чаще всего такая нейтральная логика очень проста – ничего не делать. Таким образом, нам больше не нужно иметь дело со специальной обработкой нулевых ссылок.

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

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

3. UML-диаграмма шаблона нулевого объекта

Давайте посмотрим на шаблон визуально:

Как мы видим, мы можем идентифицировать следующих участников:

    Клиенту требуется экземпляр AbstractObject AbstractObject определяет контракт, который ожидает клиент – он также может содержать общую логику для реализующих классов RealObject реализует AbstractObject и обеспечивает реальное поведение NullObject реализует AbstractObject и обеспечивает нейтральное поведение

4. Реализация

Теперь, когда у нас есть четкое представление о теории, давайте посмотрим на пример.

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

Однако время от времени в наше приложение могут приходить сообщения с «неопределенным» или пустым приоритетом. Такие сообщения должны быть исключены из дальнейшей обработки.

Во-первых, мы создадим интерфейс Router:

public interface Router {
    void route(Message msg);
}

Затем давайте создадим две реализации вышеприведенного интерфейса — одну, отвечающую за маршрутизацию к SMS-шлюзу, и другую, которая будет направлять сообщения на Очередь JMS:

public class SmsRouter implements Router {
    @Override
    public void route(Message msg) {
        // implementation details
    }
}
public class JmsRouter implements Router {
    @Override
    public void route(Message msg) {
        // implementation details
    }
}

public class NullRouter implements Router {
    @Override
    public void route(Message msg) {
        // do nothing
    }
}

Наконец, давайте реализуем наш нулевой объект:

public class RoutingHandler {
    public void handle(Iterable<Message> messages) {
        for (Message msg : messages) {
            Router router = RouterFactory.getRouterForMessage(msg);
            router.route(msg);
        }
    }
}

Теперь мы готовы собрать все части вместе. Давайте посмотрим, как может выглядеть пример клиентского кода:

Как мы видим, мы обрабатываем все объекты Router одинаково, независимо от того, какую реализацию возвращает RouterFactory. Это позволяет нам сохранять наш код чистым и читабельным.

5. Когда использовать шаблон нулевого объекта

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

Такой подход следует общим принципам объектно-ориентированного подхода, таким как «Говори-не-спрашивай».

public interface CustomerDao {
    Collection<Customer> findByNameAndLastname(String name, String lastname);
    Customer getById(Long id);
}

Чтобы лучше понять, когда мы должны использовать шаблон нулевого объекта, давайте представим, что нам нужно реализовать интерфейс CustomerDao, определенный следующим образом:

Большинство разработчиков вернут Collections.emptyList() из findByNameAndLastname() в случае ни один из клиентов не соответствует указанным критериям поиска. Это очень хороший пример использования шаблона Null Object.

Напротив, getById() должен возвращать клиента с заданным идентификатором. Кто-то, вызывающий этот метод, ожидает получить конкретную сущность клиента. Если такого клиента не существует, мы должны явно вернуть null, чтобы сигнализировать о том, что с предоставленным идентификатором что-то не так.

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

6. Заключение

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