Всем привет.

Данная статья будет полезна начинающим Java разработчиком для понимания зачем нужен шаблон проектирования "Цепочка ответственности" и как его можно использовать на примерах.

Итак начнем с самого начала. Основная суть данного шаблона: связывание объектов-получателей в цепочку и передача запроса по ней.

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

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

Начнем с первого примера.

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

Вначале напишем enum PriorityLevel с уровнями ошибки.

public enum PriorityLevel {
    LOW, MIDDLE, HIGH
}

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

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

Далее пишем основной метод messageSenderManager(String message, PriorityLevel level), в котором и будет происходить основная логика нашего примера. Данный метод будет принимать строку, которую мы будем передавать и приоритет ошибки. Как видно из кода, если уровень передаваемого приоритета больше или равен нашему приоритету, то мы отправляем сообщение и пока имеется следующий отправитель — мы снова вызываем данный метод у него.

Также добавим абстрактный метод write(String message), который мы будем переопределять в классах наследниках.

public abstract class MessageSender {
    private PriorityLevel priorityLevel;
    private MessageSender nextMessageSender;

    public MessageSender(PriorityLevel priorityLevel) {
        this.priorityLevel = priorityLevel;
    }

    public void setNextMessageSender(MessageSender nextMessageSender) {
        this.nextMessageSender = nextMessageSender;
    }
  
    public void messageSenderManager(String message, PriorityLevel level){
        if(level.ordinal() >= priorityLevel.ordinal()){
            write(message);
        }
        if(nextMessageSender != null){
            nextMessageSender.messageSenderManager(message, level);
        }
    }

    public abstract void write(String message);
}

Далее создадим три конкретных класса по отправке сообщений: LogReportMessageSender, EmailMessageSender и SMSMessageSender, для отправки сообщений в лог‑файл, на электронную почту и отправку смс, соответственно. Каждый класс унаследуем от класса MessageSender, переопределим конструктор и метод write(), чтобы каждый класс делал свою логику в данном методе.

public class LogReportMessageSender extends MessageSender{

    public LogReportMessageSender(PriorityLevel priorityLevel) {
        super(priorityLevel);
    }

    @Override
    public void write(String message) {
        System.out.println("Message sender using simple log report: " + message);
    }
}
public class EmailMessageSender extends MessageSender{

    public EmailMessageSender(PriorityLevel priorityLevel) {
        super(priorityLevel);
    }

    @Override
    public void write(String message) {
        System.out.println("Sending email: " + message);
    }
}
public class SMSMessageSender extends MessageSender{
    public SMSMessageSender(PriorityLevel priorityLevel) {
        super(priorityLevel);
    }

    @Override
    public void write(String message) {
        System.out.println("Sending SMS to manager: " + message);
    }
}

Наш пример почти готов, осталось его протестить.

Для этого напишем класс BugEvent с методом main(String[] args).

public class BugEvent {
    public static void main(String[] args) {
        MessageSender reportMessageSender = new LogReportMessageSender(PriorityLevel.LOW);
        MessageSender emailMessageSender = new EmailMessageSender(PriorityLevel.MIDDLE);
        MessageSender smsMessageSender = new SMSMessageSender(PriorityLevel.HIGH);

        reportMessageSender.setNextMessageSender(emailMessageSender);
        emailMessageSender.setNextMessageSender(smsMessageSender);

        reportMessageSender.messageSenderManager("Something is happening!", PriorityLevel.LOW);

        System.out.println("---------------------------------------------------------------------");
        reportMessageSender.messageSenderManager("Something went wrong!", PriorityLevel.MIDDLE);

        System.out.println("---------------------------------------------------------------------");
        reportMessageSender.messageSenderManager("We had a problem!", PriorityLevel.HIGH);

    }
}

Создадим три объекта MessageSender reportMessageSender, emailMessageSender и smsMessageSender из соответствующих классов и устанавливаем им свои уровни ошибки.

Далее строим «цепочку ответственности» и назначаем объекту reportMessageSender следующего отправителя — объект emailMessageSender, а объекту emailMessageSender следующего отправителя — smsMessageSender.

Все наш шаблон готов — можно тестировать.

Выведем все три объекта в консоль с разными сообщениями.

Мы видим, что наше сообщение идет по цепочке и выполняется разными обработчиками до того момента, пока уровень ошибки меньше или равен заданному.

Еще раз посмотрим общую схему классов в данном паттерне:

У нас есть общий абстрактный класс MessageSender, с полями PriorityLevel и MessageSender и методами для установки следующего отправителя, абстрактного метода write() — переопределяемого в каждом классе и основного метода messageSenderManager(), в котором мы выполняем какое‑то действие (отправляем сообщение) и переходим к следующему отправителю.

Давайте сейчас рассмотрим другой пример использования данного паттерна.

Допустим у нас есть задание: нужно написать приложение, в которое приходит число и нужно определить отрицательное это число, это число «ноль» или это положительное число.

Да, задание очень простое и для него целого паттерна может и не надо использовать, но данный пример в простом виде может показать как какой‑то реквест будет «идти» по цепочке до тех пор, пока не найдется получатель, который его обработает.

Как и в предыдущем случае начнем с основного класса, назовем его Handler.

public abstract class Handler {
    protected Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public abstract void handleRequest(int request);
}

Данный класс будет проще, чем предыдущий пример. В качестве поля мы вводим следующего обработчика Handler next, делаем метод setNext(Handler next) — для назначения следующего обработчика, а также абстрактный метод handleRequest(int request), который принимает наш реквест — это будет число. Реализация данного метода будет представлена в каждом классе унаследованном от класса Handler.

Напишем класс NegativeNumberHandler для обработки отрицательных чисел.

public class NegativeNumberHandler extends Handler{
    public void handleRequest(int request) {
        if (request < 0) {
            System.out.println("NegativeNumberHandler handled the request. Number is negative: " + request);
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
}

Унаследуем наш класс от класса Handler.

Логика переопределенного метода handleRequest(int request) очень простая, если переданное число меньше 0, то мы обрабатываем этот рексест и метод останавливается в противном случае мы переходим к следующему обработчикку.

Таким же образом напишем еще два класса ZeroNumberHandler.

public class ZeroNumberHandler extends Handler{
    public void handleRequest(int request) {
        if (request == 0) {
            System.out.println("ZeroNumberHandler handled the request. Number is zero: " + request);
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
}

и PositiveNumberHandler.

public class PositiveNumberHandler extends Handler{
    public void handleRequest(int request) {
        if (request > 0) {
            System.out.println("PositiveNumberHandler handled the request. Number is positive: " + request);
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
}

Наш второй пример почти готов. Напишем еще класс Client для тестирования этого шаблона.

public class Client {
    public static void main(String[] args) {
        Handler negativeNumberHandler = new NegativeNumberHandler();
        Handler zeroNumberHandler = new ZeroNumberHandler();
        Handler positiveNumberHandler = new PositiveNumberHandler();

        negativeNumberHandler.setNext(zeroNumberHandler);
        zeroNumberHandler.setNext(positiveNumberHandler);

        int[] requests = {-2, 5, 14, 22, -18, 3, 0, -20};

        Arrays.stream(requests).forEach(negativeNumberHandler::handleRequest);
    }
}

Здесь как и в предыдущем случае вначале создаем три новых объекта Handler на основании трех классов NegativeNumberHandler, ZeroNumberHandler и PositiveNumberHandler.

Далее строим «цепочку ответственности» и назначаем объекту negativeNumberHandler следующего обработчика zeroNumberHandler, а для объекта zeroNumberHandler назначаем следующего обработчика positiveNumberHandler.

Все наш второй пример с шаблоном «цепочка ответственности» готов.

Выполним наш шаблон в восьмью разными цифрами.

Мы видим, что наш реквест (число) идет по цепочке до тех пор, пока не обработается соответствующим обработчиком.

Рассмотрим еще раз схему классов в данном примере.

У нас есть основной абстрактный класс Handler в котором есть поле Handler next, метод setNext(Handler next), в котором мы устанавливаем следующего обработчика, а также абстрактный метод handleRequest(int request), в котором будет происходить вся логика и каждый класс, унаследованный от класса Handler, будет в данном методе брать приходящий реквест, и если он соответствует условию, то он выполняется и дальше не передается, а если условие не выполняется, то реквест идет дальше по цепочке к следующему обработчику.

Вот мы и дошли до конца в вопросе с шаблоном «Цепочка ответственности».

В конце хочу еще обратить внимание на отрицательные стороны использования данного шаблона:

  1. Шаблон «цепочка ответственности» может привести к более сложной структуре кода, чем альтернативные подходы.

  2. Можно создать циклические ссылки в цепочке, если next ссылки не назначаются тщательно. Это может привести к бесконечным циклам или другому неожиданному поведению программы.

  3. Шаблон «цепочка ответственности» может затруднить определение того какой Handler объект отвечает за обработку конкретного запроса. Это может затруднить отладку кода и понимания его поведения.

Спасибо Всем кто дочитал до конца. Всем пока!

Комментарии (1)


  1. OlegZH
    08.04.2023 13:05

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

    Лично для меня вопросы возникают уже в самом начале рассуждений:

    Вначале напишем enum PriorityLevel с уровнями ошибки.

    Почему здесь нужен enum? А если мы придумаем новый уровень ошибок, то придётся править в нескольких местах?

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

    1. Шаблон «цепочка ответственности» может затруднить определение того какой Handler объект отвечает за обработку конкретного запроса. Это может затруднить отладку кода и понимания его поведения.

    Вот было бы здорово, если бы изнутри кода можно было бы узнавать, исполняется ли код в отладочном режиме, или он исполняется в боевом режиме! Здесь могла бы выть своя «цепочка ответственности», где по цепочке передаётся текущий обработчик.

    Но!

    Это всё — конечные автоматы!!

    Почему нельзя саму программу сразу делать в конечных автоматах (например, по Шалыто) ?!?