Javascript. Command pattern

Паттерн Команда (Command pattern) инкапсулирует запрос в виде объекта, делая возможной параметризацию клиентских объектов с другими запросами, организацию очереди или регистрацию запросов, а также поддержку отмены операций. Данный паттерн относится к поведенческим паттернам.
Объект команды инкапсулирует запрос посредством привязки набора операций к конкретному получателю.  Для этого информация об операции и получателе "упаковывается" в объекте с методами execute() и undo(). При вызове метод execute() выполняет операцию с данным получателем (Receivers). Метод undo() выполняет обратную операцию (отмену). Внешние объекты (Invokers) не знают, какие именно операции выполняются, и с каким получателем; они только знают, что при вызове методов execute() или undo() их запросы будут выполнены.
На практике нередко встречаются "умные" объекты команд, которые реализуют запрос самостоятельно вместо его делегирования получателю. Команды также могут использоваться для реализации систем регистрации команд и поддержки транзакций.


Ниже приведен пример реализации данного паттерна.

/* Receiver */

var Calculator = function () {

    var result = 0;

    // add operation
    this.add = function (value) {
        result += value;
    };
   
    // subtract operation
    this.subtract = function (value) {
        result -= value;
    };
   
    // multiply operation
    this.multiply = function (value) {
        result *= value;
    };
   
    // divide operation
    this.divide = function (value) {
        if (value === 0) {
            throw new Error("Divide by zero is forbidden");
        }
        result /= value;
    };
   
    // set common result
    this.setResult = function (value) {
        result = value;
    };
   
    // get common result
    this.getResult = function () {
        return result;
    };

};

/* interface Command */

var Command = function () {
    this.execute = function () {};
    this.undo = function () {};
};

/* ConcreteCommand - AddCommand */

var AddCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.add(value);
    };
   
    this.undo = function () {
        calculator.subtract(value);
    };
};

AddCommand.prototype = new Command();
AddCommand.prototype.constructor = AddCommand;

/* ConcreteCommand - SubtractCommand */

var SubtractCommand = function (calculator, value) {

    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.subtract(value);
    };
   
    this.undo = function () {
        calculator.add(value);
    };

};

SubtractCommand.prototype = new Command();
SubtractCommand.prototype.constructor = SubtractCommand;

/* ConcreteCommand - MultiplyCommand */

var MultiplyCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult;
   
    this.execute = function () {
        result = calculator.getResult();
        calculator.multiply(value);
    };
   
    this.undo = function () {
        if (value === 0) {
          
        }
        calculator.divide(value);
    };
};

MultiplyCommand.prototype = new Command();
MultiplyCommand.prototype.constructor = MultiplyCommand;

/* ConcreteCommand - DivideCommand */

var DivideCommand = function (calculator, value) {

    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.divide(value);
    };
   
    this.undo = function () {
        calculator.multiply(value);
    };

};

DivideCommand.prototype = new Command();
DivideCommand.prototype.constructor = DivideCommand;

/* Invoker */

var User = function () {

    var calculator = new Calculator();
        commands = [],
        command = null;
   
    this.add = function (value) {
        command = new AddCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.subtract = function (value) {
        command = new SubtractCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.multiply = function (value) {
        command = new MultiplyCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.divide = function (value) {
        command = new DivideCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.undo = function () {
        if (commands.length === 0) {
            throw new Error("Command stack is empty");
        }
        command = commands.pop();
        command.undo();
    };
   
    this.getCount = function () {
        return calculator.getResult();
    };
   
};


// testing
var accountant = new User();
accountant.add(5);
accountant.subtract(4);
console.log(accountant.getCount()); // 1


Данная реализация имеет один существенный недостаток: предположим, что одной из операций будет accountant.multiply(0), и в случае, если мы захотим откатить результат, то команда MultiplyCommand попытает сделать запрос undo(), но как известно на ноль делить нельзя и на выходе получим неприятное сообщение "Divide by zero is forbidden". То есть результат не будет откатан к первоначальному виду, одно из решений может быть таким:

/* Receiver */

var Calculator = function () {

    var result = 0;

    // add operation
    this.add = function (value) {
        result += value;
    };
   
    // subtract operation
    this.subtract = function (value) {
        result -= value;
    };
   
    // multiply operation
    this.multiply = function (value) {
        result *= value;
    };
   
    // divide operation
    this.divide = function (value) {
        if (value === 0) {
            throw new Error("Divide by zero is forbidden");
        }
        result /= value;
    };
   
    // set common result
    this.setResult = function (value) {
        result = value;
    };
   
    // get common result
    this.getResult = function () {
        return result;
    };

};

/* interface Command */

var Command = function () {
    this.execute = function () {};
    this.undo = function () {};
};

/* ConcreteCommand - AddCommand */

var AddCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.add(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };
   
};

AddCommand.prototype = new Command();
AddCommand.prototype.constructor = AddCommand;

/* ConcreteCommand - SubtractCommand */

var SubtractCommand = function (calculator, value) {

    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.subtract(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };

};

SubtractCommand.prototype = new Command();
SubtractCommand.prototype.constructor = SubtractCommand;

/* ConcreteCommand - MultiplyCommand */

var MultiplyCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.multiply(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };
   
};

MultiplyCommand.prototype = new Command();
MultiplyCommand.prototype.constructor = MultiplyCommand;

/* ConcreteCommand - DivideCommand */

var DivideCommand = function (calculator, value) {

    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();;
   
    this.execute = function () {
        calculator.divide(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };

};

DivideCommand.prototype = new Command();
DivideCommand.prototype.constructor = DivideCommand;

/* Invoker */

var User = function () {

    var calculator = new Calculator();
        commands = [],
        command = null;
   
    this.add = function (value) {
        command = new AddCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.subtract = function (value) {
        command = new SubtractCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.multiply = function (value) {
        command = new MultiplyCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.divide = function (value) {
        command = new DivideCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.undo = function () {
        if (commands.length === 0) {
            throw new Error("Command stack is empty");
        }
        command = commands.pop();
        command.undo();
    };
   
    this.getCount = function () {
        return calculator.getResult();
    };
   
};

// testing
var accountant = new User();
accountant.add(5);
accountant.add(10);
accountant.add(1);
accountant.multiply(0);
accountant.undo();
console.log(accountant.getCount()); // 16


Как видим, все отрабатывает без ошибок. Вот еще один простой пример:

/* interface Command */

var Command = function () {
    this.execute = function () {};
};

/* Receiver - Light */

var Light = function () {
    this.on = function () {
        console.log("The light is on");
    };
    this.off = function () {
        console.log("The light is off");
    };
};

/* Concrete Command - LightOnCommand */

var LightOnCommand = function (light) {
    var light = light;
    this.execute = function () {
        light.on();
    };
};

LightOnCommand.prototype = new Command();
LightOnCommand.prototype.constructor = LightOnCommand;

/* Concrete Command - LightOffCommand */

var LightOffCommand = function (light) {
    var light = light;
    this.execute = function () {
        light.off();
    };
};

LightOffCommand.prototype = new Command();
LightOffCommand.prototype.constructor = LightOffCommand;

/* Invoker - SimpleSwitcher */

var SimpleSwitcher = function () {
  
    var onComand,
        offCommand;
  
    this.setCommand = function (onCmd, offCmd) {
        onCommand = onCmd;
        offCommand = offCmd;
    };
  
    this.onButton = function () {
        onCommand.execute();
    };
  
    this.offButton = function () {
        offCommand.execute();
    };
  
};

/* test application */

var Application = function () {

    this.run = function () {
  
        // light switcher
        var switcher = new SimpleSwitcher();
      
        //
        var light = new Light();
      
        var lightOnCommand = new LightOnCommand(light);
        var lightOffCommand = new LightOffCommand(light);
      
        switcher.setCommand(lightOnCommand, lightOffCommand);
      
        switcher.onButton();
        switcher.offButton();
  
    };

};

var application = new Application();
application.run();


На выходе:

The light is on
The light is off


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