解释器模式的原理和实现
命令模式的原理解读命令模式的英文翻译是 Command Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:
The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations.
翻译成中文就是下面这样。为了帮助你理解,我对这个翻译稍微做了补充和解释,也一起放在了下面的括号中。
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
假设我们定义了一个新的加减乘除计算“语言”,语法规则如下:运算符只包含加、减、乘、除,并且没有优先级的概念;表达式(也就是前面提到的“句子”)中,先书写数字,后书写运算符,空格隔开;按照先后顺序,取出两个数字和一个运算符计算结果,结果重新放入数字的最头部位置,循环上述过程,直到只剩下一个数字,这个数字就是表达式最终的计算结果。
比如“ 8 3 2 4 - + * ”这样一个表达式,我们按照上面的语法规则来处理,取出数字 “8 3” 和“-”运算符,计算得到 5,于是表达式就变成了“ 5 2 4 + * ”。然后,我们再取出“ 5 2 ”和“ + ”运算符,计算得到 7,表达式就变成了“ 7 4 * ”。最后,我们取出“ 7 4”和“ * ”运算符,最终得到的结果就是 28。
解释器模式的代码实现比较灵活,没有固定的模板。我们前面也说过,应用设计模式主要是应对代码的复杂性,实际上,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
前面定义的语法规则有两类表达式,一类是数字,一类是运算符,运算符又包括加减乘除。利用解释器模式,我们把解析的工作拆分到 NumberExpression、AdditionExpression、SubstractionExpression、MultiplicationExpression、DivisionExpression 这样五个解析类中。
按照这个思路,我们对代码进行重构,重构之后的代码如下所示。当然,因为加减乘除表达式的解析比较简单,利用解释器模式的设计思路,看起来有点过度设计。不过呢,这里我主要是为了解释原理,你明白意思就好,不用过度细究这个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 public interface Expression { long interpret () ; } public class NumberExpression implements Expression { private long number; public NumberExpression (long number) { this .number = number; } public NumberExpression (String number) { this .number = Long.parseLong(number); } @Override public long interpret () { return this .number; } } public class AdditionExpression implements Expression { private Expression exp1; private Expression exp2; public AdditionExpression (Expression exp1, Expression exp2) { this .exp1 = exp1; this .exp2 = exp2; } @Override public long interpret () { return exp1.interpret() + exp2.interpret(); } } public class ExpressionInterpreter { private Deque<Expression> numbers = new LinkedList <>(); public long interpret (String expression) { String[] elements = expression.split(" " ); int length = elements.length; for (int i = 0 ; i < (length+1 )/2 ; ++i) { numbers.addLast(new NumberExpression (elements[i])); } for (int i = (length+1 )/2 ; i < length; ++i) { String operator = elements[i]; boolean isValid = "+" .equals(operator) || "-" .equals(operator) || "*" .equals(operator) || "/" .equals(operator); if (!isValid) { throw new RuntimeException ("Expression is invalid: " + expression); } Expression exp1 = numbers.pollFirst(); Expression exp2 = numbers.pollFirst(); Expression combinedExp = null ; if (operator.equals("+" )) { combinedExp = new AdditionExpression (exp1, exp2); } else if (operator.equals("-" )) { combinedExp = new AdditionExpression (exp1, exp2); } else if (operator.equals("*" )) { combinedExp = new AdditionExpression (exp1, exp2); } else if (operator.equals("/" )) { combinedExp = new AdditionExpression (exp1, exp2); } long result = combinedExp.interpret(); numbers.addFirst(new NumberExpression (result)); } if (numbers.size() != 1 ) { throw new RuntimeException ("Expression is invalid: " + expression); } return numbers.pop().interpret(); } }
解释器模式实战举例
接下来,我们再来看一个更加接近实战的例子,也就是咱们今天标题中的问题:如何实现一个自定义接口告警规则功能?
为了简化讲解和代码实现,我们假设自定义的告警规则只包含“||、&&、>、<、”这五个运算符,其中,“>、<、 ”运算符的优先级高于“||、&&”运算符,“&&”运算符优先级高于“||”。在表达式中,任意元素之间需要通过空格来分隔。除此之外,用户可以自定义要监控的 key。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class AlertRuleInterpreter { public AlertRuleInterpreter (String ruleExpression) { } public boolean interpret (Map<String, Long> stats) { } } public class DemoTest { public static void main (String[] args) { String rule = "key1 > 100 && key2 < 30 || key3 < 100 || key4 == 88" ; AlertRuleInterpreter interpreter = new AlertRuleInterpreter (rule); Map<String, Long> stats = new HashMap <>(); stats.put("key1" , 101l ); stats.put("key3" , 121l ); stats.put("key4" , 88l ); boolean alert = interpreter.interpret(stats); System.out.println(alert); } }
实际上,我们可以把自定义的告警规则,看作一种特殊“语言”的语法规则。我们实现一个解释器,能够根据规则,针对用户输入的数据,判断是否触发告警。利用解释器模式,我们把解析表达式的逻辑拆分到各个小类中,避免大而复杂的大类的出现。按照这个实现思路,我把刚刚的代码补全,如下所示,你可以拿你写的代码跟我写的对比一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 public interface Expression { boolean interpret (Map<String, Long> stats) ; } public class GreaterExpression implements Expression { private String key; private long value; public GreaterExpression (String strExpression) { String[] elements = strExpression.trim().split("\\s+" ); if (elements.length != 3 || !elements[1 ].trim().equals(">" )) { throw new RuntimeException ("Expression is invalid: " + strExpression); } this .key = elements[0 ].trim(); this .value = Long.parseLong(elements[2 ].trim()); } public GreaterExpression (String key, long value) { this .key = key; this .value = value; } @Override public boolean interpret (Map<String, Long> stats) { if (!stats.containsKey(key)) { return false ; } long statValue = stats.get(key); return statValue > value; } } public class AndExpression implements Expression { private List<Expression> expressions = new ArrayList <>(); public AndExpression (String strAndExpression) { String[] strExpressions = strAndExpression.split("&&" ); for (String strExpr : strExpressions) { if (strExpr.contains(">" )) { expressions.add(new GreaterExpression (strExpr)); } else if (strExpr.contains("<" )) { expressions.add(new LessExpression (strExpr)); } else if (strExpr.contains("==" )) { expressions.add(new EqualExpression (strExpr)); } else { throw new RuntimeException ("Expression is invalid: " + strAndExpression); } } } public AndExpression (List<Expression> expressions) { this .expressions.addAll(expressions); } @Override public boolean interpret (Map<String, Long> stats) { for (Expression expr : expressions) { if (!expr.interpret(stats)) { return false ; } } return true ; } } public class OrExpression implements Expression { private List<Expression> expressions = new ArrayList <>(); public OrExpression (String strOrExpression) { String[] andExpressions = strOrExpression.split("\\|\\|" ); for (String andExpr : andExpressions) { expressions.add(new AndExpression (andExpr)); } } public OrExpression (List<Expression> expressions) { this .expressions.addAll(expressions); } @Override public boolean interpret (Map<String, Long> stats) { for (Expression expr : expressions) { if (expr.interpret(stats)) { return true ; } } return false ; } } public class AlertRuleInterpreter { private Expression expression; public AlertRuleInterpreter (String ruleExpression) { this .expression = new OrExpression (ruleExpression); } public boolean interpret (Map<String, Long> stats) { return expression.interpret(stats); } }
在告警规则解析的例子中,如果我们要在表达式中支持括号“()”,那如何对代码进行重构呢?你可以把它当作练习,试着编写一下代码。
重点回顾
解释器模式的代码实现比较灵活,没有固定的模板。我们前面说过,应用设计模式主要是应对代码的复杂性,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
参考
设计模式之美_设计模式_代码重构-极客时间
https://time.geekbang.org/column/intro/250