Command Pattern

Definition

Encapsulate a request as an object, thereby letting you parameterize clients with different requests,
queue or log requests, and support undoable operations.

Explanation

In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time. This information includes the method name, the object that owns the method and values for the method parameters.

Four terms always associated with the command pattern are command, receiver, invoker and client. A command object has a receiver object and invokes a method of the receiver in a way that is specific to that receiver’s class. The receiver then does the work. A command object is separately passed to an invoker object, which invokes the command, and optionally does bookkeeping about the command execution. Any command object can be passed to the same invoker object. Both an invoker object and several command objects are held by a client object. The client contains the decision making about which commands to execute at which points. To execute a command, it passes the command object to the invoker object. See example code below.

Using command objects makes it easier to construct general components that need to delegate, sequence or execute method calls at a time of their choosing without the need to know the class of the method or the method parameters. Using an invoker object allows bookkeeping about command executions to be conveniently performed, as well as implementing different modes for commands, which are managed by the invoker object, without the need for the client to be aware of the existence of bookkeeping or modes.

Screencast

TypeScript Code

module Command {
    interface ICommand {
        execute(): void;
        undo(): void;
    }

    class StyleCommand implements ICommand {
        private previousValue: string;
        constructor(private receiver: HTMLElement,
                    private propertyName: string,
                    private value: string) {
        }

        execute() {
            this.previousValue = this.receiver.style[this.propertyName];
            this.receiver.style[this.propertyName] = this.value;
        }

        undo() {
            this.receiver.style[this.propertyName] = this.previousValue;
        }
    }

    class BackgroundColorCommand extends StyleCommand {
        constructor(receiver: HTMLElement, value: string) {
            super(receiver, "backgroundColor", value);
        }
    }

    class TextAlignCommand extends StyleCommand {
        constructor(receiver: HTMLElement, value: string) {
            super(receiver, "textAlign", value);
        }
    }

    class Invoker {
        private commands = new Array();
        private current = -1;

        constructor() {
        }

        ExecuteCommand(command: ICommand) {
            if (this.commands.length - 1 > this.current) {
                var next = this.current + 1
                this.commands.splice(next, this.commands.length - next);
            }

            this.commands.push(command);
            command.execute();
            this.current = this.commands.length - 1;
        }

        public undo() {
            if (this.current >= 0) {
                this.commands[this.current--].undo();
            }
        }

        public redo() {
            if (this.current < this.commands.length - 1) {
                this.commands[++this.current].execute();
            }
        }
    }

    window.addEventListener("load", function () {
        var ribbon = document.getElementById("commandRibbon");
        var receiver = document.getElementById("receiver");
        var invoker = new Invoker();

        var undoRedoButtons = ButtonFactory.createUndoRedoButtons(invoker);
        ribbon.appendChild(undoRedoButtons);

        var textAlignButtons = ButtonFactory.createTextAlignButtons(receiver, invoker);
        ribbon.appendChild(textAlignButtons);

        var colorButtons = ButtonFactory.createColorButtons(receiver, invoker);
        ribbon.appendChild(colorButtons);
    });


    class ButtonFactory {
        static createColorButtons(receiver: HTMLElement, invoker: Invoker): HTMLDivElement {
            var colorButtons = document.createElement("DIV");
            colorButtons.className = "colorButtons";

            var clr = new Array('00', '40', '80', 'c0', 'ff');
            var clrLength = clr.length;
            for (var i = 0; i < clrLength; i++) {
                var r = "#" + clr[i];
                for (var j = 0; j < clrLength; j++) {
                    var rg = r + clr[j];
                    for (var k = 0; k < clrLength; k++) {
                        var rgb = rg + clr[k];
                        var colorButton = ButtonFactory.createButton(null, rgb);
                        colorButton.style.backgroundColor = rgb;
                        colorButtons.appendChild(colorButton);
                    }
                }
            }

            colorButtons.addEventListener("click", (event) => {
                var elem = event.target;
                var elem = event.target;
                if (elem.tagName === "BUTTON") {
                    event.preventDefault();
                    var button = elem;
                    var command = new BackgroundColorCommand(receiver, button.value);
                    invoker.ExecuteCommand(command);
                }
            });

            return colorButtons;
        }

        static createTextAlignButtons(receiver: HTMLElement, invoker: Invoker): HTMLDivElement {
            var textAlignButtons = document.createElement("DIV");
            textAlignButtons.className = "textAlignButtons";

            textAlignButtons.appendChild(ButtonFactory.createButton("Left", "left"));
            textAlignButtons.appendChild(ButtonFactory.createButton("Center", "center"));
            textAlignButtons.appendChild(ButtonFactory.createButton("Right", "right"));

            textAlignButtons.addEventListener("click", (event) => {
                var elem = event.target;
                if (elem.tagName === "BUTTON") {
                    event.preventDefault();
                    var button = elem;
                    var command = new TextAlignCommand(receiver, button.value);
                    invoker.ExecuteCommand(command);
                }
            });

            return textAlignButtons;
        }

        static createUndoRedoButtons(invoker: Invoker): HTMLDivElement {
            var undoRedoButtons = document.createElement("DIV");
            undoRedoButtons.className = "undoRedoButtons";

            undoRedoButtons.appendChild(ButtonFactory.createButton("Undo", "undo"));
            undoRedoButtons.appendChild(ButtonFactory.createButton("Redo", "redo"));

            undoRedoButtons.addEventListener("click", (event) => {
                var elem = event.target;
                if (elem.tagName === "BUTTON") {
                    var button = elem;
                    event.preventDefault();
                    var value = button.value;
                    switch (value) {
                        case "undo":
                            invoker.undo();
                            break;
                        default:
                            invoker.redo();
                    }
                }
            });

            return undoRedoButtons;
        }

        static createButton(text?: string, value?: string): HTMLButtonElement {
            var button = document.createElement("BUTTON");
            button.type = "button";
            if (text) {
                button.textContent = text;
            }

            if (value) {
                button.value = value;
            }

            return button;
        }
    }
}

Output

Click any of the above buttons to change the background color or alignment of this element.
Use the Undo and Redo buttons to move up and down the command tree.