Статьи

Шаблоны команд в Spring Framework


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

Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения

Командный шаблон является одним из 11 шаблонов дизайна семейства паттернов «  Банды четырех»  . Мы уже узнали о   паттерне цепочки ответственности , и, аналогично этому и другим паттернам семейства, паттерн команд описывает обязанности объектов в приложении и то, как они взаимодействуют между ними.

Шаблон команды: Введение

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

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

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

Командный объект в основном имеет   метод execute () и необязательный   метод undo () . Метод  execute ()  выполняет некоторую операцию с получателем, а метод  undo ()  отменяет текущую операцию. Выполнение операции осуществляется получателем. Вызывающий только устанавливает себя с объектом команды и вызывает команду, вызывая метод  execute ()  . Вызывающий не знает, что  будет делать метод execute () .

Представьте, что invoker является переключателем. Когда invoker вызывает команду, вызывая метод  execute ()  объекта команды, он может включить свет или даже запустить генератор. По сути, это означает, что один и тот же invoker (switch) может использоваться для вызова команд для выполнения действий на разных приемниках (устройствах). Нам нужно только создать соответствующий объект команды и установить его на вызывающем. Как видно, применяя шаблон команд, мы можем перенести возможность многократного использования на другой уровень, и это стало возможным благодаря слабой связи между вызывающим и получателем и объектом команды, который действует как интерфейс между ними.

Участники Командной Выкройки

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

Представьте себе компанию по производству игрушек, которая недавно решила производить игрушки с дистанционным управлением. Они начали с машины с дистанционным управлением. Пульт дистанционного управления имеет три кнопки: Вкл., Выкл. И Отмена. Пульт дистанционного управления запрограммирован таким образом, что кнопка «Вкл» перемещает автомобиль, кнопка «Выкл.» Останавливает его, а кнопка «Отмена» изменяет текущее действие автомобиля. Автомобиль с дистанционным управлением имел успех, и затем руководство приняло решение изготовить вращающийся верх с дистанционным управлением. Снова, вместе с верхом, был изготовлен новый пульт дистанционного управления, запрограммированный для запуска и остановки вращения и реверсирования текущего действия верха. Руководствуясь успехом игрушек с дистанционным управлением, руководство решило изготовить 200 разновидностей игрушек с дистанционным управлением, каждая из которых была запрограммирована по-разному для управления соответствующей игрушкой.Представьте себя в команде программистов. Вам необходимо создать 200 различных программ для пультов дистанционного управления, каждая из которых предназначена для работы с определенной игрушкой. Видимо плохое дизайнерское решение из-за невозможности повторного использования кода. Причина также очевидна. Каждый пульт дистанционного управления тесно связан с игрушкой, на которой он работает. Нам нужен пульт дистанционного управления, который запрограммирован для работы со всеми игрушками, и вот тут на помощь приходит модель командования.и вот где командный паттерн приходит на помощь.и вот где командный паттерн приходит на помощь.

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

Далее мы смоделируем команды, которые будут запускать действия на приемниках. Мы начнем с  интерфейса CommandBase  с двумя методами  execute ()  и  undo () . Каждый конкретный подкласс  CommandBase  представляет команду для запуска действия на получателе, и для этого он поддерживает ссылку на получателя, действие которого он инициирует. Для   приемников Car  и  RotatingTop давайте смоделируем конкретные классы команд как CarMoveCommandCarStopCommandTopRotateCommand и  TopStopRotateCommand . Наконец, мы смоделируем invoker как  RemoteControl учебный класс. Этот класс не знает ни о каком конкретном командном классе. Всякий раз, когда нажимается одна из ее кнопок, метод, обрабатывающий событие нажатия, вводится с помощью соответствующего объекта команды во время выполнения, и вызывающий вызывает для него метод  execute ()  .

В контексте примера игрушек с дистанционным управлением давайте подведем итоги участникам шаблона командования.

  • Command  ( CommandBase ): интерфейс для выполнения действия.
  • ConcreteCommand  ( CarMoveCommandCarStopCommandTopRotateCommand и TopStopRotateCommand ): являются конкретными классами, которые реализуют  Command  и определяют   методы execute ()  и undo () для взаимодействия с получателями для выполнения действия и его отмены соответственно.
  • Invoker ( RemoteControl ): просит  командование  выполнить действие.
  • Receiver  ( Car  and  RotatingTop ): выполняет действие в зависимости от получаемой команды.
  • Клиент : Создает   объект ConcreteCommand и устанавливает его получателя.

На следующем рисунке показаны отношения между участниками.

Участники Командной Выкройки

Применение шаблона команды

Давайте реализуем пример игрушек с дистанционным управлением в коде Java. Начнем с приемников.

Car.java

package guru.springframework.gof.command.receiver;
 
 
public class Car {
    public void move()
    {
        System.out.println("Car is moving");
    }
    public void stop()
    {
        System.out.println("Car has stopped");
    }
}

RotatingTop.java

package guru.springframework.gof.command.receiver;
 
 
public class RotatingTop {
    public void startRotating(){
        System.out.println("Top has start rotating");
    }
    public void stopRotating(){
        System.out.println("Top has stopped rotating");
    }
}

Оба указанных выше класса приемника определяют методы, имена которых не требуют пояснений, для выполнения действий.

Теперь давайте напишем команды. Начнем с   интерфейса CommandBase .

CommandBase.java

package guru.springframework.gof.command.commandobjects;
 
 
public interface CommandBase {
    void execute();
    void undo();
}

В  CommandBase выше интерфейс, мы объявили  execute() и  undo() методы , что ConcreteCommand  классы будут осуществлять для вызова действия на ресиверах. У нас есть четыре действия, два действия для каждого получателя. Поэтому мы напишем четыре   класса ConcreteCommand для вызова четырех действий.

CarMoveCommand.java

package guru.springframework.gof.command.commandobjects;
 
import guru.springframework.gof.command.receiver.Car;
 
public class CarMoveCommand implements CommandBase {
    private Car car;
    public CarMoveCommand(Car car){
        this.car=car;
    }
    @Override
    public void execute(){
       System.out.println("CarMoveCommand.execute(): Invoking move() on Car");
        car.move();
    }
    @Override
    public void undo(){
      System.out.println("CarMoveCommand.undo():  Undoing previous action->Invoking stop() on Car");
      car.stop();
 
    }
}

CarStopCommand.java

package guru.springframework.gof.command.commandobjects;
 
 
import guru.springframework.gof.command.receiver.Car;
 
public class CarStopCommand implements CommandBase{
    private Car car;
    public CarStopCommand(Car car){
        this.car=car;
    }
    @Override
    public void execute(){
        System.out.println("CarStopCommand.execute(): Invoking stop() on Car");
        car.stop();
    }
    @Override
    public void undo()
    {
        System.out.println("CarStopCommand.undo(): Undoing previous action-> Invoking move() on Car");
        car.move();
    }
}

TopRotateCommand.java

package guru.springframework.gof.command.commandobjects;
 
 
import guru.springframework.gof.command.receiver.RotatingTop;
 
public class TopRotateCommand implements CommandBase{
    RotatingTop rotatingTop;
    public  TopRotateCommand(RotatingTop rotatingTop){
        this.rotatingTop=rotatingTop;
    }
    @Override
    public void execute(){
     System.out.println("TopRotateCommand.execute(): Invoking startRotating() on RotatingTop");
      rotatingTop.startRotating();
    }
    @Override
    public void undo(){
        System.out.println("TopRotateCommand.undo(): Undoing previous action->Invoking stopRotating() on RotatingTop");
        rotatingTop.stopRotating();
    }
}

TopStopRotateCommand.java

package guru.springframework.gof.command.commandobjects;
 
 
import guru.springframework.gof.command.receiver.RotatingTop;
 
public class TopStopRotateCommand implements CommandBase{
    RotatingTop rotatingTop;
    public TopStopRotateCommand(RotatingTop rotatingTop){
        this.rotatingTop=rotatingTop;
    }
    @Override
    public void execute(){
        System.out.println("TopStopRotateCommand.execute(): Invoking stopRotating() on RotatingTop");
        rotatingTop.stopRotating();
    }
    @Override
    public void undo(){
        System.out.println("TopStopRotateCommand.undo(): Undoing previous action->Invoking startRotating() on RotatingTop");
        rotatingTop.startRotating();
    }
}

Each of the preceding ConcreteCommand classes maintains a reference to its receiver and the reference is initialized through the constructor. In the overridden execute() method, we called the corresponding method of the receiver that implements the action to be performed. In the undo() method we reversed the action performed through execute(). With our commands and receivers set, we will next write the invoker – the RemoteControl class.

RemoteControl.java

package guru.springframework.gof.command.invoker;
 
 
import guru.springframework.gof.command.commandobjects.CommandBase;
 
public class RemoteControl {
    CommandBase onCommand, offCommand, undoCommand;
 
    public void onButtonPressed(CommandBase onCommand){
       this.onCommand=onCommand;
       onCommand.execute();
       undoCommand=onCommand;
    }
 
    public void offButtonPressed(CommandBase offCommand){
        this.offCommand=offCommand;
        offCommand.execute();
        undoCommand=offCommand;
    }
    public void undoButtonPressed(){
         undoCommand.undo();
    }
 
}

In the RemoteControl class above, we wrote the onButtonPressed() method that accepts a CommandBase object. The ConcreteCommand object will be passed to the method at runtime. The method then calls the execute() method on the ConcreteCommand object.

As you can observe, the onButtonPressed() method does not know what ConcreteCommand object will be passed to it neither which receiver will perform the action. It can be the Car, the RotatingTop, or any other receiver we add later. In Line 12 we assigned the command object passed to onButtonPressed() to the undoCommand variable, which is of type CommandBase. This will help us track the current command being issued in order to undo the action carried out by the command whenever required.

The offButtonPressed() method is same as the onButtonPressed() method.

In the undoButtonPressed() method we called the undo() method on undoCommand. At runtime theundo() method of the ConcreteCommand object that undoCommand is currently assigned to will get invoked.

Let’s write a test class and see how our remote control works. We will first create the commands for the Carreceiver and load the RemoteControl object with the commands to test how the remote control operates the car. We will next load the RemoteControl object with commands for the RotatingTop receiver.

RemoteControlTest.java

package guru.springframework.gof.command.invoker;
 
import guru.springframework.gof.command.commandobjects.*;
import guru.springframework.gof.command.receiver.Car;
import guru.springframework.gof.command.receiver.RotatingTop;
import org.junit.Test;
 
import static org.junit.Assert.*;
 
public class RemoteControlTest {
    @Test
    public void testRemoteControlButtonPressed() throws Exception {
        RemoteControl remoteControl=new RemoteControl();
        System.out.println("-----Testing onButtonPressed on RemoteControl for Car-----");
        Car car=new Car();
        CommandBase carMoveCommand=new CarMoveCommand(car);
        remoteControl.onButtonPressed(carMoveCommand);
        System.out.println("-----Testing offButtonPressed on RemoteControl for Car-----");
        CommandBase carStopCommand=new CarStopCommand(car);
        remoteControl.offButtonPressed(carStopCommand);
        System.out.println("-----Testing undoButtonPressed() on RemoteControl for Car-----");
        remoteControl.undoButtonPressed();
 
        System.out.println("-----Testing onButtonPressed on RemoteControl for RotatingTop-----");
        RotatingTop top=new RotatingTop();
        CommandBase topRotateCommand=new TopRotateCommand(top);
        remoteControl.onButtonPressed(topRotateCommand);
 
        System.out.println("-----Testing offButtonPressed on RemoteControl for RotatingTop-----");
        CommandBase topStopRotateCommand=new TopStopRotateCommand(top);
        remoteControl.offButtonPressed(topStopRotateCommand);
 
       System.out.println("-----Testing undoButtonPressed on RemoteControl for RotatingTop-----");
 
        remoteControl.undoButtonPressed();
 
 
    }
 
 
}

In the test method, we created a RemoteControl object. From Line 15 – Line 17 we created a Car object, then created a CarMoveCommand object initialized with the Car object. We then called the onButtonPressed() method of RemoteControl passing the CarMoveCommand object. Similarly, from Line 19 – Line 20, we created a CarStopCommand object initialized with the same Car object we created earlier, and then called the offButtonPressed() method of RemoteControl passing the CarStopCommand object. To test the undo functionality, we called the undo() method of RemoteControl in Line 22. Similarly, from Line 25 -31 we wrote the code to create a RotatingTop object, initialized the corresponding ConcreteCommand objects (TopRotateCommand and TopStopRotateCommand) with RotatingToy, and then called the onButtonPressed() and offButtonPressed() methods of RemoteControl, passing the appropriate ConcreteCommandobjects. We also called the undo() method of RemoteControl in Line 35 to test the undo functionality onRotatingToy. The important thing to observe here is that we are using the same RemoteControl object to operate on both the receivers. So the next time we add a toy (receiver), we only need to create itsConcreteCommand classes and pass it to RemoteControl. The RemoteControl remains unchanged. The output of the code is this.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running guru.springframework.gof.command.invoker.RemoteControlTest
-----Testing onButtonPressed on RemoteControl for Car-----
CarMoveCommand.execute(): Invoking move() on Car
Car is moving
-----Testing offButtonPressed on RemoteControl for Car-----
CarStopCommand.execute(): Invoking stop() on Car
Car has stopped
-----Testing undoButtonPressed() on RemoteControl for Car-----
CarStopCommand.undo(): Undoing previous action-> Invoking move() on Car
Car is moving
-----Testing onButtonPressed on RemoteControl for RotatingTop-----
TopRotateCommand.execute(): Invoking startRotating() on RotatingTop
Top has start rotating
-----Testing offButtonPressed on RemoteControl for RotatingTop-----
TopStopRotateCommand.execute(): Invoking stopRotating() on RotatingTop
Top has stopped rotating
-----Testing undoButtonPressed on RemoteControl for RotatingTop-----
TopStopRotateCommand.undo(): Undoing previous action->Invoking startRotating() on RotatingTop
Top has start rotating
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 sec - in guru.springframework.gof.command.invoker.RemoteControlTest

Summary

Beginning programmers may find the Command Pattern daunting. They may not have the vision nor experience to see how it applies to common programming tasks. The Command Pattern is commonly seen in GUI code, such as handling actions of buttons, menu items, action links and also Java progress bars and wizards. It is also seen in Runnable related code. But, this pattern is not limited to them. In developing web applications with Spring MVC, you often see the concepts of the Command Pattern applied through the use of Command Objects. Think about a ecommerce application where you are adding an item to your cart. That form post to add the item to your cart is effectively a “command”. Hence a core component of Spring MVC is the AbstractCommandController.

The Command Pattern isn’t just limited to Spring MVC. When developing enterprise applications using the Spring Framework, you will find plenty of other opportunities to apply the Command Pattern. Whenever you are writing code that require some invoker to perform different actions on multiple receivers, consider using the Command Pattern – after all it is one of the classic GoF design pattern.