场景

leetcode 65 题 判断是否是合法的数字:

image-20220204102426236

部分有效数字列举如下:["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"] 部分无效数字列举如下:["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]

我们可以依次遍历给定的字符串,然后各种 ifelse 来解决这个问题:

1/**
2 * @param {string} s
3 * @return {boolean}
4 */
5const isNumber = function (s) {
6  const e = ['e', 'E']
7  s = s.trim()
8
9  let pointSeen = false
10  let eSeen = false
11  let numberSeen = false
12  let numberAfterE = true
13  for (let i = 0; i < s.length; i++) {
14    if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
15      numberSeen = true
16      numberAfterE = true
17    }
18    else if (s.charAt(i) === '.') {
19      if (eSeen || pointSeen)
20        return false
21
22      pointSeen = true
23    }
24    else if (e.includes(s.charAt(i))) {
25      if (eSeen || !numberSeen)
26        return false
27
28      numberAfterE = false
29      eSeen = true
30    }
31    else if (s.charAt(i) === '-' || s.charAt(i) === '+') {
32      if (i != 0 && !e.includes(s.charAt(i - 1)))
33        return false
34    }
35    else {
36      return false
37    }
38  }
39
40  return numberSeen && numberAfterE
41}

如果只是为了刷题 AC 也没啥毛病,但如果在业务中写出这么多 ifelse 大概就要被打了。

为了让代码扩展性和可读性更高,我们可以通过责任链模式进行改写。

责任链模式

GoF 介绍的责任链模式定义:

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.

避免请求者和接收者之间的耦合,让多个接收者都有机会去处理请求。将接收者组成链条,在链条中传递请求直到有接收者可以处理它。

原始的定义中,当请求被处理后链条就终止了,但很多地方也会将请求一直传递下去,可以看作是责任链模式的变体。

看一下 UML 类图和时序图:

image-20220204151213707

Sender 无需关心哪一个 Receiver 去处理它,只需要通过 Handler 接口在 Receiver 链条中进行处理,每一个 Receiver 处理结束后继续传给下一个 Receiver

看起来比较抽象,看一个具体的例子,不同等级的日志进行不同的处理:

1import java.util.*;
2
3abstract class Logger
4{
5    public static int ERR = 3;
6    public static int NOTICE = 5;
7    public static int DEBUG = 7;
8    protected int mask;
9
10    // The next element in the chain of responsibility
11    protected Logger next;
12    public Logger setNext( Logger l)
13    {
14        next = l;
15        return this;
16    }
17
18    public final void message( String msg, int priority )
19    {
20        if ( priority <= mask )
21        {
22            writeMessage( msg );
23            if ( next != null )
24            {
25                next.message( msg, priority );
26            }
27        }
28    }
29
30    protected abstract void writeMessage( String msg );
31
32}
33
34class StdoutLogger extends Logger
35{
36
37    public StdoutLogger( int mask ) { this.mask = mask; }
38
39    protected void writeMessage( String msg )
40    {
41        System.out.println( "Writting to stdout: " + msg );
42    }
43}
44
45class EmailLogger extends Logger
46{
47
48    public EmailLogger( int mask ) { this.mask = mask; }
49
50    protected void writeMessage( String msg )
51    {
52        System.out.println( "Sending via email: " + msg );
53    }
54}
55
56class StderrLogger extends Logger
57{
58
59    public StderrLogger( int mask ) { this.mask = mask; }
60
61    protected void writeMessage( String msg )
62    {
63        System.out.println( "Sending to stderr: " + msg );
64    }
65}
66
67public class ChainOfResponsibilityExample
68{
69    public static void main( String[] args )
70    {
71        // Build the chain of responsibility
72        Logger l = new StdoutLogger( Logger.DEBUG).setNext(
73                            new EmailLogger( Logger.NOTICE ).setNext(
74                            new StderrLogger( Logger.ERR ) ) );
75
76        // Handled by StdoutLogger
77        l.message( "Entering function y.", Logger.DEBUG );
78
79        // Handled by StdoutLogger and EmailLogger
80        l.message( "Step1 completed.", Logger.NOTICE );
81
82        // Handled by all three loggers
83        l.message( "An error has occurred.", Logger.ERR );
84    }
85}

输出:

1Writting to stdout: Entering function y.
2Writting to stdout: Step1 completed.
3Sending via email: Step1 completed.
4Writting to stdout: An error has occurred.
5Sending via email: An error has occurred.
6Sending to stderr: An error has occurred.

每个 logger 都继承了 message 方法,并且拥有的 next 也指向一个 logger 对象,通过 next 去调用下一个的 message 方法。

image-20220204152750255

让我们用 js 再来改写一下:

我们先实现一个 Handler 对象,构建链条。

1const Handler = function (fn) {
2  this.handler = fn
3  this.next = null
4}
5
6Handler.prototype.setNext = function setNext(h) {
7  this.next = h
8  return h
9}
10
11Handler.prototype.passRequest = function () {
12  const ret = this.handler.apply(this, arguments)
13  this.next && this.next.passRequest.apply(this.next, arguments)
14}

接下来实现不同的 Logger

1const ERR = 3
2const NOTICE = 5
3const DEBUG = 7
4
5const StdoutLogger = function (msg, level) {
6  // 根据等级判断自己是否处理
7  if (level <= DEBUG)
8    console.log(`Writting to stdout: ${msg}`)
9}
10
11const EmailLogger = function (msg, level) {
12  // 根据等级判断自己是否处理
13  if (level <= NOTICE)
14    console.log(`Sending via email: ${msg}`)
15}
16
17const StderrLogger = function (msg, level) {
18  // 根据等级判断自己是否处理
19  if (level <= ERR)
20    console.log(`Sending to stderr: ${msg}`)
21}

然后进行测试:

1const StdoutHandler = new Handler(StdoutLogger)
2const EmailHandler = new Handler(EmailLogger)
3const StderrHandler = new Handler(StderrLogger)
4StdoutHandler.setNext(EmailHandler).setNext(StderrHandler)
5
6StdoutHandler.passRequest('Entering function y.', DEBUG)
7StdoutHandler.passRequest('Step1 completed.', NOTICE)
8StdoutHandler.passRequest('An error has occurred.', ERR)

输出内容和 java 代码是一致的。

代码实现

回到开头的场景中,判断是否是有效数字。

我们可以抽离出不同功能,判断是否是整数、是否是科学记数法、是否是浮点数等等,然后通过职责链模式把它们链接起来,如果某一环节返回了 true 就不再判断,直接返回最终结果。

可以利用上边写的 Handler 对象,构建链条,此外可以通过返回值提前结束传递。

1function Handler(fn) {
2  this.handler = fn
3  this.next = null
4}
5
6Handler.prototype.setNext = function setNext(h) {
7  this.next = h
8  return h
9}
10
11Handler.prototype.passRequest = function () {
12  const ret = this.handler.apply(this, arguments)
13  // 提前结束
14  if (ret)
15    return ret
16
17  // 向后传递
18  if (this.next)
19    return this.next.passRequest.apply(this.next, arguments)
20
21  return ret
22}

数字预处理一下,去掉前后空白和 +- 便于后续的判断。

1function preProcessing(v) {
2  let value = v.trim()
3  if (value.startsWith('+') || value.startsWith('-'))
4    value = value.substring(1)
5
6  return value
7}

判断是否是整数:

1// 判断是否是整数
2function isInteger(integer) {
3  integer = preProcessing(integer)
4  if (!integer)
5    return false
6
7  for (let i = 0; i < integer.length; i++) {
8    if (!/[0-9]/.test(integer.charAt(i)))
9      return false
10  }
11
12  return true
13}

判断是否是小数:

1// 判断是否是小数
2function isFloat(floatVal) {
3  floatVal = preProcessing(floatVal)
4  if (!floatVal)
5    return false
6
7  function checkPart(part) {
8    if (part === '')
9      return true
10
11    if (!/[0-9]/.test(part.charAt(0)) || !/[0-9]/.test(part.charAt(part.length - 1)))
12      return false
13
14    if (!isInteger(part))
15      return false
16
17    return true
18  }
19  const pos = floatVal.indexOf('.')
20  if (pos === -1)
21    return false
22
23  if (floatVal.length === 1)
24    return false
25
26  const first = floatVal.substring(0, pos)
27  const second = floatVal.substring(pos + 1, floatVal.length)
28
29  if (checkPart(first) && checkPart(second))
30    return true
31
32  return false
33}

判断是否是科学计数法:

1// 判断是否是科学计数法
2function isScienceFormat(s) {
3  s = preProcessing(s)
4  if (!s)
5    return false
6
7  function checkHeadAndEndForSpace(part) {
8    if (part.startsWith(' ') || part.endsWith(' '))
9      return false
10
11    return true
12  }
13  function validatePartBeforeE(first) {
14    if (!first)
15      return false
16
17    if (!checkHeadAndEndForSpace(first))
18      return false
19
20    if (!isInteger(first) && !isFloat(first))
21      return false
22
23    return true
24  }
25
26  function validatePartAfterE(second) {
27    if (!second)
28      return false
29
30    if (!checkHeadAndEndForSpace(second))
31      return false
32
33    if (!isInteger(second))
34      return false
35
36    return true
37  }
38  s = s.toLowerCase()
39  const pos = s.indexOf('e')
40  if (pos === -1)
41    return false
42
43  if (s.length === 1)
44    return false
45
46  const first = s.substring(0, pos)
47  const second = s.substring(pos + 1, s.length)
48
49  if (!validatePartBeforeE(first) || !validatePartAfterE(second))
50    return false
51
52  return true
53}

判断是否是十六进制:

1function isHex(hex) {
2  function isValidChar(c) {
3    const validChar = ['a', 'b', 'c', 'd', 'e', 'f']
4    for (let i = 0; i < validChar.length; i++) {
5      if (c === validChar[i])
6        return true
7    }
8
9    return false
10  }
11  hex = preProcessing(hex)
12  if (!hex)
13    return false
14
15  hex = hex.toLowerCase()
16  if (hex.startsWith('0x'))
17    hex = hex.substring(2)
18  else
19    return false
20
21  for (let i = 0; i < hex.length; i++) {
22    if (!/[0-9]/.test(hex.charAt(0)) && !isValidChar(hex.charAt(i)))
23      return false
24  }
25
26  return true
27}

然后通过 Handler 将上边的功能串联起来即可:

1/**
2 * @param {string} s
3 * @return {boolean}
4 */
5const isNumber = function (s) {
6  const isIntegerHandler = new Handler(isInteger)
7  const isFloatHandler = new Handler(isFloat)
8  const isScienceFormatHandler = new Handler(isScienceFormat)
9  const isHexHandler = new Handler(isHex)
10
11  isIntegerHandler.setNext(isFloatHandler).setNext(isScienceFormatHandler).setNext(isHexHandler)
12
13  return isIntegerHandler.passRequest(s)
14}

通过责任链的设计模式,每一个函数都可以很好的进行复用,并且未来如果要新增一种类型判断,只需要加到责任链中即可,和之前的判断也完全独立。

易混设计模式

说到沿着「链」执行,应该会想到 装饰器模式

image-20220204202859919

它和责任链模式看起来结构上是一致的,我的理解上主要有两点不同:

  1. 装饰器模式是对已有功能的增强,依次包装起来形成链式调用。而责任链模式从一开始就抽象出了很多功能,然后形成责任链。
  2. 装饰器模式会依次调用新增的功能直到最初的功能,责任链模式提供了一种中断的能力,调用到某个操作的时候可以直接终止掉,不是所有的功能都会调用。

当处理一件事情的时候发现会分很多种情况去讨论,此时可以考虑使用责任链模式进行功能的拆分,提高代码的复用性、扩展性以及可读性。

js 中底层的原型链、作用域链、Dom 元素的冒泡机制都可以看作是责任链模式的应用。