The code has been written for several years, and the design pattern is in a state of forgetting and forgetting. Recently, I have some feelings about the design pattern, so I will learn and summarize it again.

Most speak design pattern articles are using Java, c + + such based on the class the static type of language, as a front-end developer, js this prototype based dynamic language, function has become a first class citizen, slightly different on some design patterns, even simple to don’t like to use the design patterns, sometimes also can produce some confusion.

The following are summarized in the order of “scenario” – “Design pattern definition” – “code implementation” – “Mixable design pattern” – “total”. If there are any inappropriate points, welcome to discuss.

scenario

Leetcode 65 判断 whether the number is valid:

Some significant figures are listed below: [” 2 “, “0089”, “0.1”, “+ 3.14”, “4”, “- 9”, “2 e10”, “- 90 e3”, “3 e + 7”, “+ 6 e – 1”, “53.5 e93”, “123.456 e789]” invalid part Numbers listed below: [” ABC “, “1 a”, “1 e”, “e3”, “99 e2. 5”, “- 6”, “- + 3”, “95 a54e53”]

We can solve this problem by iterating through a given string and then iterating through various ifs and else’s:

/ * * *@param {string} s
 * @return {boolean}* /
const isNumber = function (s) {
    const e = ["e"."E"];
    s = s.trim();

    let pointSeen = false;
    let eSeen = false;
    let numberSeen = false;
    let numberAfterE = true;
    for (let i = 0; i < s.length; i++) {
        if ("0" <= s.charAt(i) && s.charAt(i) <= "9") {
            numberSeen = true;
            numberAfterE = true;
        } else if (s.charAt(i) === ".") {
            if (eSeen || pointSeen) {
                return false;
            }
            pointSeen = true;
        } else if (e.includes(s.charAt(i))) {
            if(eSeen || ! numberSeen) {return false;
            }
            numberAfterE = false;
            eSeen = true;
        } else if (s.charAt(i) === "-" || s.charAt(i) === "+") {
            if(i ! =0 && !e.includes(s.charAt(i - 1))) {
                return false; }}else {
            return false; }}return numberSeen && numberAfterE;
};
Copy the code

There’s nothing wrong with AC if you just want to brush the question, but if you write so many if’s and else’s in business, you’re probably going to get slapped.

To make the code more extensible and readable, we can rewrite it through the chain of responsibility pattern.

Design Pattern definition

GoF introduces the definition of the chain of responsibility pattern:

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.

Avoid coupling between requester and receiver, giving multiple receivers the opportunity to process the request. Chain the receiver and pass the request in the chain until a receiver can process it.

In the original definition, the chain was terminated when the request was processed, but in many places the request was also passed along, which can be seen as a variation of the chain of responsibility model.

Take a look at the UML class diagram and sequence diagram:

The Sender does not need to care which Receiver handles it, just needs to process it in the Receiver chain through the Handler interface, and after each Receiver finishes processing it continues to the next Receiver.

It may seem abstract, but let’s look at a concrete example where different levels of logs are processed differently:

import java.util.*;

abstract class Logger 
{
    public static int ERR = 3;
    public static int NOTICE = 5;
    public static int DEBUG = 7;
    protected int mask;

    // The next element in the chain of responsibility
    protected Logger next;
    public Logger setNext( Logger l)
    {
        next = l;
        return this;
    }

    public final void message( String msg, int priority )
    {
        if ( priority <= mask ) 
        {
            writeMessage( msg );
            if( next ! =null) { next.message( msg, priority ); }}}protected abstract void writeMessage( String msg );

}

class StdoutLogger extends Logger 
{

    public StdoutLogger( int mask ) { this.mask = mask; }

    protected void writeMessage( String msg )
    {
        System.out.println( "Writting to stdout: "+ msg ); }}class EmailLogger extends Logger 
{

    public EmailLogger( int mask ) { this.mask = mask; }

    protected void writeMessage( String msg )
    {
        System.out.println( "Sending via email: "+ msg ); }}class StderrLogger extends Logger 
{

    public StderrLogger( int mask ) { this.mask = mask; }

    protected void writeMessage( String msg )
    {
        System.out.println( "Sending to stderr: "+ msg ); }}public class ChainOfResponsibilityExample
{
    public static void main( String[] args )
    {
        // Build the chain of responsibility
        Logger l = new StdoutLogger( Logger.DEBUG).setNext(
                            new EmailLogger( Logger.NOTICE ).setNext(
                            new StderrLogger( Logger.ERR ) ) );

        // Handled by StdoutLogger
        l.message( "Entering function y.", Logger.DEBUG );

        // Handled by StdoutLogger and EmailLogger
        l.message( "Step1 completed.", Logger.NOTICE );

        // Handled by all three loggers
        l.message( "An error has occurred.", Logger.ERR ); }}Copy the code

Output:

Writting to stdout: Entering function y.
Writting to stdout: Step1 completed.
Sending via email: Step1 completed.
Writting to stdout: An error has occurred.
Sending via email: An error has occurred.
Sending to stderr: An error has occurred.
Copy the code

Each logger inherits the Message method and has a Next that points to a Logger object, which calls the next Message method.

Let’s rewrite this with js:

We’ll start by implementing a Handler object to build the chain.

const Handler = function (fn) {
    this.handler = fn;
    this.next = null;
};

Handler.prototype.setNext = function setNext(h) {
    this.next = h;
    return h;
};

Handler.prototype.passRequest = function () {
    const ret = this.handler.apply(this.arguments);
    this.next && this.next.passRequest.apply(this.next, arguments);
};

Copy the code

Next, implement the different loggers.

const ERR = 3;
const NOTICE = 5;
const DEBUG = 7;

const StdoutLogger = function (msg, level) {
    // Determine whether to process according to the level
    if (level <= DEBUG) {
        console.log("Writting to stdout: "+ msg); }};const EmailLogger = function (msg, level) {
  	// Determine whether to process according to the level
    if (level <= NOTICE) {
        console.log("Sending via email: "+ msg); }};const StderrLogger = function (msg, level) {
  	// Determine whether to process according to the level
    if (level <= ERR) {
        console.log("Sending to stderr: "+ msg); }};Copy the code

Then test it:

const StdoutHandler = new Handler(StdoutLogger);
const EmailHandler = new Handler(EmailLogger);
const StderrHandler = new Handler(StderrLogger);
StdoutHandler.setNext(EmailHandler).setNext(StderrHandler);

StdoutHandler.passRequest("Entering function y.", DEBUG);
StdoutHandler.passRequest("Step1 completed.", NOTICE);
StdoutHandler.passRequest("An error has occurred.", ERR);
Copy the code

The output is consistent with the Java code.

Code implementation

Go back to the opening scenario and determine if it’s a valid number.

We can separate out different functions, determine whether they are integers, scientific notation, floating-point numbers, etc., and then link them together in a chain of responsibilities pattern. If something returns true, we stop judging and return the final result.

You can build the chain using the Handler object written above, and you can end the pass early by returning a value.

function Handler(fn) {
    this.handler = fn;
    this.next = null;
}

Handler.prototype.setNext = function setNext(h) {
    this.next = h;
    return h;
};

Handler.prototype.passRequest = function () {
    const ret = this.handler.apply(this.arguments);
    // End early
    if (ret) {
        return ret;
    }
  
  	// pass backwards
    if (this.next) {
        return this.next.passRequest.apply(this.next, arguments);
    }
    return ret;
};
Copy the code

The numbers are preprocessed to remove the blanks and + and – for subsequent judgment.

function preProcessing(v) {
    let value = v.trim();
    if (value.startsWith("+") || value.startsWith("-")) {
        value = value.substring(1);
    }
    return value;
}
Copy the code

Check whether it is an integer:

// Check whether it is an integer
function isInteger(integer) {
    integer = preProcessing(integer);
    if(! integer) {return false;
    }
    for (let i = 0; i < integer.length; i++) {
        if (!/ [0-9].test(integer.charAt(i))) {
            return false; }}return true;
}
Copy the code

Determine if it is a decimal:

// Check if it is a decimal
function isFloat(floatVal) {
    floatVal = preProcessing(floatVal);
    if(! floatVal) {return false;
    }
    function checkPart(part) {
        if (part === "") {
            return true;
        }
        if(!/ [0-9].test(part.charAt(0) | |!/ [0-9].test(part.charAt(part.length - 1))) {return false;
        }

        if(! isInteger(part)) {return false;
        }

        return true;
    }
    const pos = floatVal.indexOf(".");
    if (pos === -1) {
        return false;
    }

    if (floatVal.length === 1) {
        return false;
    }

    const first = floatVal.substring(0, pos);
    const second = floatVal.substring(pos + 1, floatVal.length);

    if (checkPart(first) && checkPart(second)) {
        return true;
    }

    return false;
}
Copy the code

To determine if it is a scientific notation:

// Determine if it is a scientific notation
function isScienceFormat(s) {
    s = preProcessing(s);
    if(! s) {return false;
    }
    function checkHeadAndEndForSpace(part) {
        if (part.startsWith("") || part.endsWith("")) {
            return false;
        }

        return true;
    }
    function validatePartBeforeE(first) {
        if(! first) {return false;
        }

        if(! checkHeadAndEndForSpace(first)) {return false;
        }

        if(! isInteger(first) && ! isFloat(first)) {return false;
        }

        return true;
    }

    function validatePartAfterE(second) {
        if(! second) {return false;
        }

        if(! checkHeadAndEndForSpace(second)) {return false;
        }

        if(! isInteger(second)) {return false;
        }

        return true;
    }
    s = s.toLowerCase();
    let pos = s.indexOf("e");
    if (pos === -1) {
        pos = s.indexOf("E");
    }
    if (pos === -1) {
        return false;
    }

    if (s.length === 1) {
        return false;
    }

    const first = s.substring(0, pos);
    const second = s.substring(pos + 1, s.length);

    if(! validatePartBeforeE(first) || ! validatePartAfterE(second)) {return false;
    }

    return true;
}
Copy the code

Check whether it is hexadecimal:

function isHex(hex) {
    function isValidChar(c) {
        const validChar = ["a"."b"."c"."d"."e"."f"];
        for (let i = 0; i < validChar.length; i++) {
            if (c === validChar[i]) {
                return true; }}return false;
    }
    hex = preProcessing(hex);
    if(! hex) {return false;
    }
    hex = hex.toLowerCase();
    if (hex.startsWith("0x")) {
        hex = hex.substring(2);
    } else {
        return false;
    }

    for (let i = 0; i < hex.length; i++) {
        if (!/ [0-9].test(hex.charAt(0)) && !isValidChar(hex.charAt(i))) {
            return false; }}return true;
}
Copy the code

Then use Handler to concatenate the above functions:

/ * * *@param {string} s
 * @return {boolean}* /
const isNumber = function (s) {
    const isIntegerHandler = new Handler(isInteger);
    const isFloatHandler = new Handler(isFloat);
    const isScienceFormatHandler = new Handler(isScienceFormat);
    const isHexHandler = new Handler(isHex);

    isIntegerHandler
        .setNext(isFloatHandler)
        .setNext(isScienceFormatHandler)
        .setNext(isHexHandler);

    return isIntegerHandler.passRequest(s);
};
Copy the code

Through the design mode of responsibility chain, each function can be reused well, and if a new type judgment is added to the responsibility chain in the future, it only needs to be added, which is completely independent from the previous judgment.

Mixable design patterns

When you think of executing along a “chain,” you should think of the decorator pattern.

It seems to be structurally consistent with the chain of responsibility model, but there are two main differences in my understanding:

  1. Decorator patterns are enhancements to existing functions wrapped in sequence to form chain calls. The responsibility chain model abstracts many functions from the beginning, and then forms the responsibility chain.
  2. The decorator mode calls the newly added functions up to the original one. The chain of responsibility mode provides the ability to interrupt when an operation is called. Not all functions are called.

The total

When dealing with one thing, it is found that there are many cases to discuss, at this time, we can consider using the chain of responsibility mode to split functions, and improve the reusability, extensibility and readability of the code.

The basic prototype chain, scope chain and Dom element bubbling mechanism in JS can be regarded as the application of the chain of responsibility pattern.

More design patterns recommended reading:

Front end design mode series – strategic mode

Front-end design mode series – proxy mode

Front end design mode series – decorator mode

Front end design mode series – Observer mode

Front end design pattern series – publish subscribe pattern