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

部分有效数字列举如下:["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"]
我们可以依次遍历给定的字符串,然后各种 if
、else
来解决这个问题:
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
也没啥毛病,但如果在业务中写出这么多 if
、else
大概就要被打了。
为了让代码扩展性和可读性更高,我们可以通过责任链模式进行改写。
责任链模式
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
类图和时序图:

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
方法。

让我们用 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}
通过责任链的设计模式,每一个函数都可以很好的进行复用,并且未来如果要新增一种类型判断,只需要加到责任链中即可,和之前的判断也完全独立。
易混设计模式
说到沿着「链」执行,应该会想到 装饰器模式 。

它和责任链模式看起来结构上是一致的,我的理解上主要有两点不同:
- 装饰器模式是对已有功能的增强,依次包装起来形成链式调用。而责任链模式从一开始就抽象出了很多功能,然后形成责任链。
- 装饰器模式会依次调用新增的功能直到最初的功能,责任链模式提供了一种中断的能力,调用到某个操作的时候可以直接终止掉,不是所有的功能都会调用。
总
当处理一件事情的时候发现会分很多种情况去讨论,此时可以考虑使用责任链模式进行功能的拆分,提高代码的复用性、扩展性以及可读性。
像 js
中底层的原型链、作用域链、Dom
元素的冒泡机制都可以看作是责任链模式的应用。