CategoryResourceRepost/极客时间专栏/设计模式之美/设计模式与范式:行为型/72 | 解释器模式:如何设计实现一个自定义接口告警规则功能?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

361 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="72 | 解释器模式:如何设计实现一个自定义接口告警规则功能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/04/f9/0413f0c64e70074f61d0e2f2163924f9.mp3"></audio>
上一节课,我们学习了命令模式。命令模式将请求封装成对象,方便作为函数参数传递和赋值给变量。它主要的应用场景是给命令的执行附加功能,换句话说,就是控制命令的执行,比如,排队、异步、延迟执行命令、给命令执行记录日志、撤销重做命令等等。总体上来讲,命令模式的应用范围并不广。
今天,我们来学习解释器模式,它用来描述如何构建一个简单的“语言”解释器。比起命令模式,解释器模式更加小众,只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。所以,解释器模式也不是我们学习的重点,你稍微了解一下就可以了。
话不多说,让我们正式开始今天的学习吧!
## 解释器模式的原理和实现
解释器模式的英文翻译是Interpreter Design Pattern。在GoF的《设计模式》一书中它是这样定义的
>
Interpreter pattern is used to defines a grammatical representation for a language and provides an interpreter to deal with this grammar.
翻译成中文就是:解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
看了定义,你估计会一头雾水,因为这里面有很多我们平时开发中很少接触的概念,比如“语言”“语法”“解释器”。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。
要想了解“语言”表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。
为了让你更好地理解定义,我举一个比较贴近生活的例子来解释一下。
实际上理解这个概念我们可以类比中英文翻译。我们知道把英文翻译成中文是有一定规则的。这个规则就是定义中的“语法”。我们开发一个类似Google Translate这样的翻译器这个翻译器能够根据语法规则将输入的中文翻译成英文。这里的翻译器就是解释器模式定义中的“解释器”。
刚刚翻译器这个例子比较贴近生活,现在,我们再举个更加贴近编程的例子。
假设我们定义了一个新的加减乘除计算“语言”,语法规则如下:
- 运算符只包含加、减、乘、除,并且没有优先级的概念;
- 表达式(也就是前面提到的“句子”)中,先书写数字,后书写运算符,空格隔开;
- 按照先后顺序,取出两个数字和一个运算符计算结果,结果重新放入数字的最头部位置,循环上述过程,直到只剩下一个数字,这个数字就是表达式最终的计算结果。
我们举个例子来解释一下上面的语法规则。
比如“ 8 3 2 4 - + * ”这样一个表达式我们按照上面的语法规则来处理取出数字“8 3”和“-”运算符计算得到5于是表达式就变成了“ 5 2 4 + * ”。然后,我们再取出“ 5 2 ”和“ + ”运算符计算得到7表达式就变成了“ 7 4 * ”。最后,我们取出“ 7 4”和“ * ”运算符最终得到的结果就是28。
看懂了上面的语法规则我们将它用代码实现出来如下所示。代码非常简单用户按照上面的规则书写表达式传递给interpret()函数,就可以得到最终的计算结果。
```
public class ExpressionInterpreter {
private Deque&lt;Long&gt; numbers = new LinkedList&lt;&gt;();
public long interpret(String expression) {
String[] elements = expression.split(&quot; &quot;);
int length = elements.length;
for (int i = 0; i &lt; (length+1)/2; ++i) {
numbers.addLast(Long.parseLong(elements[i]));
}
for (int i = (length+1)/2; i &lt; length; ++i) {
String operator = elements[i];
boolean isValid = &quot;+&quot;.equals(operator) || &quot;-&quot;.equals(operator)
|| &quot;*&quot;.equals(operator) || &quot;/&quot;.equals(operator);
if (!isValid) {
throw new RuntimeException(&quot;Expression is invalid: &quot; + expression);
}
long number1 = numbers.pollFirst();
long number2 = numbers.pollFirst();
long result = 0;
if (operator.equals(&quot;+&quot;)) {
result = number1 + number2;
} else if (operator.equals(&quot;-&quot;)) {
result = number1 - number2;
} else if (operator.equals(&quot;*&quot;)) {
result = number1 * number2;
} else if (operator.equals(&quot;/&quot;)) {
result = number1 / number2;
}
numbers.addFirst(result);
}
if (numbers.size() != 1) {
throw new RuntimeException(&quot;Expression is invalid: &quot; + expression);
}
return numbers.pop();
}
}
```
在上面的代码实现中语法规则的解析逻辑第23、25、27、29行都集中在一个函数中对于简单的语法规则的解析这样的设计就足够了。但是对于复杂的语法规则的解析逻辑复杂代码量多所有的解析逻辑都耦合在一个函数中这样显然是不合适的。这个时候我们就要考虑拆分代码将解析逻辑拆分到独立的小类中。
该怎么拆分呢?我们可以借助解释器模式。
解释器模式的代码实现比较灵活,没有固定的模板。我们前面也说过,应用设计模式主要是应对代码的复杂性,实际上,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
前面定义的语法规则有两类表达式一类是数字一类是运算符运算符又包括加减乘除。利用解释器模式我们把解析的工作拆分到NumberExpression、AdditionExpression、SubstractionExpression、MultiplicationExpression、DivisionExpression这样五个解析类中。
按照这个思路,我们对代码进行重构,重构之后的代码如下所示。当然,因为加减乘除表达式的解析比较简单,利用解释器模式的设计思路,看起来有点过度设计。不过呢,这里我主要是为了解释原理,你明白意思就好,不用过度细究这个例子。
```
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();
}
}
// SubstractionExpression/MultiplicationExpression/DivisionExpression与AdditionExpression代码结构类似这里就省略了
public class ExpressionInterpreter {
private Deque&lt;Expression&gt; numbers = new LinkedList&lt;&gt;();
public long interpret(String expression) {
String[] elements = expression.split(&quot; &quot;);
int length = elements.length;
for (int i = 0; i &lt; (length+1)/2; ++i) {
numbers.addLast(new NumberExpression(elements[i]));
}
for (int i = (length+1)/2; i &lt; length; ++i) {
String operator = elements[i];
boolean isValid = &quot;+&quot;.equals(operator) || &quot;-&quot;.equals(operator)
|| &quot;*&quot;.equals(operator) || &quot;/&quot;.equals(operator);
if (!isValid) {
throw new RuntimeException(&quot;Expression is invalid: &quot; + expression);
}
Expression exp1 = numbers.pollFirst();
Expression exp2 = numbers.pollFirst();
Expression combinedExp = null;
if (operator.equals(&quot;+&quot;)) {
combinedExp = new AdditionExpression(exp1, exp2);
} else if (operator.equals(&quot;-&quot;)) {
combinedExp = new AdditionExpression(exp1, exp2);
} else if (operator.equals(&quot;*&quot;)) {
combinedExp = new AdditionExpression(exp1, exp2);
} else if (operator.equals(&quot;/&quot;)) {
combinedExp = new AdditionExpression(exp1, exp2);
}
long result = combinedExp.interpret();
numbers.addFirst(new NumberExpression(result));
}
if (numbers.size() != 1) {
throw new RuntimeException(&quot;Expression is invalid: &quot; + expression);
}
return numbers.pop().interpret();
}
}
```
## 解释器模式实战举例
接下来,我们再来看一个更加接近实战的例子,也就是咱们今天标题中的问题:如何实现一个自定义接口告警规则功能?
在我们平时的项目开发中监控系统非常重要它可以时刻监控业务系统的运行情况及时将异常报告给开发者。比如如果每分钟接口出错数超过100监控系统就通过短信、微信、邮件等方式发送告警给开发者。
一般来讲监控系统支持开发者自定义告警规则比如我们可以用下面这样一个表达式来表示一个告警规则它表达的意思是每分钟API总出错数超过100或者每分钟API总调用数超过10000就触发告警。
```
api_error_per_minute &gt; 100 || api_count_per_minute &gt; 10000
```
在监控系统中告警模块只负责根据统计数据和告警规则判断是否触发告警。至于每分钟API接口出错数、每分钟接口调用数等统计数据的计算是由其他模块来负责的。其他模块将统计数据放到一个Map中数据的格式如下所示发送给告警模块。接下来我们只关注告警模块。
```
Map&lt;String, Long&gt; apiStat = new HashMap&lt;&gt;();
apiStat.put(&quot;api_error_per_minute&quot;, 103);
apiStat.put(&quot;api_count_per_minute&quot;, 987);
```
为了简化讲解和代码实现,我们假设自定义的告警规则只包含“||、&amp;&amp;&gt;&lt;、==”这五个运算符,其中,“&gt;&lt;、==”运算符的优先级高于“||、&amp;&amp;”运算符,“&amp;&amp;”运算符优先级高于“||”。在表达式中任意元素之间需要通过空格来分隔。除此之外用户可以自定义要监控的key比如前面的api_error_per_minute、api_count_per_minute。
那如何实现上面的需求呢?我写了一个骨架代码,如下所示,其中的核心的实现我没有给出,你可以当作面试题,自己试着去补全一下,然后再看我的讲解。
```
public class AlertRuleInterpreter {
// key1 &gt; 100 &amp;&amp; key2 &lt; 1000 || key3 == 200
public AlertRuleInterpreter(String ruleExpression) {
//TODO:由你来完善
}
//&lt;String, Long&gt; apiStat = new HashMap&lt;&gt;();
//apiStat.put(&quot;key1&quot;, 103);
//apiStat.put(&quot;key2&quot;, 987);
public boolean interpret(Map&lt;String, Long&gt; stats) {
//TODO:由你来完善
}
}
public class DemoTest {
public static void main(String[] args) {
String rule = &quot;key1 &gt; 100 &amp;&amp; key2 &lt; 30 || key3 &lt; 100 || key4 == 88&quot;;
AlertRuleInterpreter interpreter = new AlertRuleInterpreter(rule);
Map&lt;String, Long&gt; stats = new HashMap&lt;&gt;();
stats.put(&quot;key1&quot;, 101l);
stats.put(&quot;key3&quot;, 121l);
stats.put(&quot;key4&quot;, 88l);
boolean alert = interpreter.interpret(stats);
System.out.println(alert);
}
}
```
实际上,我们可以把自定义的告警规则,看作一种特殊“语言”的语法规则。我们实现一个解释器,能够根据规则,针对用户输入的数据,判断是否触发告警。利用解释器模式,我们把解析表达式的逻辑拆分到各个小类中,避免大而复杂的大类的出现。按照这个实现思路,我把刚刚的代码补全,如下所示,你可以拿你写的代码跟我写的对比一下。
```
public interface Expression {
boolean interpret(Map&lt;String, Long&gt; stats);
}
public class GreaterExpression implements Expression {
private String key;
private long value;
public GreaterExpression(String strExpression) {
String[] elements = strExpression.trim().split(&quot;\\s+&quot;);
if (elements.length != 3 || !elements[1].trim().equals(&quot;&gt;&quot;)) {
throw new RuntimeException(&quot;Expression is invalid: &quot; + 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&lt;String, Long&gt; stats) {
if (!stats.containsKey(key)) {
return false;
}
long statValue = stats.get(key);
return statValue &gt; value;
}
}
// LessExpression/EqualExpression跟GreaterExpression代码类似这里就省略了
public class AndExpression implements Expression {
private List&lt;Expression&gt; expressions = new ArrayList&lt;&gt;();
public AndExpression(String strAndExpression) {
String[] strExpressions = strAndExpression.split(&quot;&amp;&amp;&quot;);
for (String strExpr : strExpressions) {
if (strExpr.contains(&quot;&gt;&quot;)) {
expressions.add(new GreaterExpression(strExpr));
} else if (strExpr.contains(&quot;&lt;&quot;)) {
expressions.add(new LessExpression(strExpr));
} else if (strExpr.contains(&quot;==&quot;)) {
expressions.add(new EqualExpression(strExpr));
} else {
throw new RuntimeException(&quot;Expression is invalid: &quot; + strAndExpression);
}
}
}
public AndExpression(List&lt;Expression&gt; expressions) {
this.expressions.addAll(expressions);
}
@Override
public boolean interpret(Map&lt;String, Long&gt; stats) {
for (Expression expr : expressions) {
if (!expr.interpret(stats)) {
return false;
}
}
return true;
}
}
public class OrExpression implements Expression {
private List&lt;Expression&gt; expressions = new ArrayList&lt;&gt;();
public OrExpression(String strOrExpression) {
String[] andExpressions = strOrExpression.split(&quot;\\|\\|&quot;);
for (String andExpr : andExpressions) {
expressions.add(new AndExpression(andExpr));
}
}
public OrExpression(List&lt;Expression&gt; expressions) {
this.expressions.addAll(expressions);
}
@Override
public boolean interpret(Map&lt;String, Long&gt; 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&lt;String, Long&gt; stats) {
return expression.interpret(stats);
}
}
```
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。
要想了解“语言”要表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。
解释器模式的代码实现比较灵活,没有固定的模板。我们前面说过,应用设计模式主要是应对代码的复杂性,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
## 课堂讨论
1.在你过往的项目经历或阅读源码的时候,有没有用到或者见过解释器模式呢?<br>
2.在告警规则解析的例子中,如果我们要在表达式中支持括号“()”,那如何对代码进行重构呢?你可以把它当作练习,试着编写一下代码。
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。