Chain Of Responsibility Pattern

Definition

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

Explanation

In object-oriented design, the chain-of-responsibility pattern is a design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain.

In a variation of the standard chain-of-responsibility model, some handlers may act as dispatchers, capable of sending commands out in a variety of directions, forming a tree of responsibility. In some cases, this can occur recursively, with processing objects calling higher-up processing objects with commands that attempt to solve some smaller part of the problem; in this case recursion continues until the command is processed, or the entire tree has been explored. An XML interpreter might work in this manner.

Screencast

TypeScript Code

module ChainOfResponsibility {
    interface ICancellableEvent {
        add(eventHandler: (sender: any, eventArgs: T) => void): void;
        remove(eventHandler: (sender: any, eventArgs: T) => void): void;
    }

    interface ICancellableEventArgs {
        cancel: boolean;
    }

    class CancellableEvent implements ICancellableEvent {
        private _eventHandlers = new Array<(sender: any, eventArgs: T) => void>();

        public add(eventHandler: (sender: any, eventArgs: T) => void): void {
            if (this._eventHandlers.indexOf(eventHandler) === -1) {
                this._eventHandlers.push(eventHandler);
            }
        }

        public remove(eventHandler: (sender: any, eventArgs: T) => void): void {
            var i = this._eventHandlers.indexOf(eventHandler);
            if (i !== -1) {
                this._eventHandlers.splice(i, 1);
            }
        }

        public raise(sender: any, e: T): void {
            var eventHandlers = this._eventHandlers.slice(0);
            for (var i = 0, j = eventHandlers.length; i < j && !e.cancel; i++) {
                try {
                    eventHandlers[i](sender, e);
                }
                catch (ex) { Output.WriteLine(ex.message); }
            }
        }
    }

    class ValidationEventArgs implements ICancellableEventArgs {
        constructor(public value: T) {
        }

        public isValid: boolean = true;
        public cancel: boolean = false;
        public result: string;

        public status() {
            if (this.isValid) {
                Output.WriteLine("The value: '" + this.value + "' is valid!");
            } else {
                Output.WriteLine(this.result);
            }
        }
    }

    class ValidationChain {
        private _validators = new CancellableEvent<ValidationEventArgs>();

        public get validators(): ICancellableEvent<ValidationEventArgs> {
            return this._validators;
        }

        public validate(eventArgs: ValidationEventArgs): void {
            this._validators.raise(this, eventArgs);
        }
    }

    class StringValidators {
        public static nullValidator = function (sender: any, eventArgs: ValidationEventArgs) {
            if (eventArgs.value === null) {
                eventArgs.result = "String is null.";
                eventArgs.cancel = true;
                eventArgs.isValid = false;
            }
        }

        public static emptyValidator = function (sender: any, eventArgs: ValidationEventArgs) {
            if (eventArgs.value.trim().length <= 0) {
                eventArgs.result = "String is empty.";
                eventArgs.cancel = true;
                eventArgs.isValid = false;
            }
        }

        public static createLengthValidator(maxLength: number) {
            return function (sender: any, eventArgs: ValidationEventArgs) {
                if (eventArgs.value.length > maxLength) {
                    eventArgs.result = "The string '" + eventArgs.value + "' is too long!";
                    eventArgs.cancel = true;
                    eventArgs.isValid = false;
                }
            }
        }
    }

    window.addEventListener("load", function () {
        var valuesToValidate: Array = [null, "", "12345678910", "Valid"];

        var validationChain = new ValidationChain();
        validationChain.validators.add(StringValidators.nullValidator);
        validationChain.validators.add(StringValidators.emptyValidator);
        validationChain.validators.add(StringValidators.createLengthValidator(10));

        valuesToValidate.forEach((value) => {
            var eventArgs = new ValidationEventArgs(value);
            validationChain.validate(eventArgs);
            eventArgs.status()
        });
    });
}

Output