编程学习哪些开发工具比较好(学了这个设计模式)
编程学习哪些开发工具比较好(学了这个设计模式)程序员,天天和开发语言打交道,和各种语法打交道。如果能从编译器角度,理解开发语言语法的设计原理,对增加我们的语感,是大有益处的。了解编译器的语法规范,就是了解语法的语法。我们才能对我们常用的编程语言的语法,有更深的理解。作为架构师,一个很重要的技能,就是技术选型,而技术选型的前提,就是眼界开阔,如果没有足够的知识广度,遇到问题,很容易找不到解决问题的方向。现在,我们就结合“诗三百”编程语言项目,给大家讲解解释器模式。在讲解之前,首先要打消一些人心中的疑虑。解释器模式,主要应用在开发编译器的项目,也就是设计编程语言的项目。这种业务场景比较冷门,一般程序员都比较陌生。大部分程序员,究其整个职业生涯,也往往不能遇到这种项目。虽然作为码农,编译器我们天天都在用,但是不知道编译器的原理,我们过得貌似也很好。但是,如果你了解了编程语言是怎么开发设计的,了解它的运行原理,了解语法的语法,你的好可能会更上
学了这个设计模式,就可以开发一个自己的编程语言玩玩;学了这个设计模式 JVM不再那么深不可测;学了这个设计模式,编译器,So easy;为了分享这个设计模式,码农老吴,给自己挖了一个深不见底的坑。
大家好,欢迎关注极客架构师,极客架构师——专注架构师成长,我是码农老吴。
本节是《架构师基本功之设计模式》第13期-第2节
在第一节,我给大家演示了,我刚刚开发的,一门运行于JVM虚拟机之上,蕴含中国传统文化基因,顺应中文写作习惯,优美雅致、韵味悠长的编程语言——诗三百,之所以开发设计这个编程语言,就是为了给大家更好的讲解,行为型设计模式中的——解释器模式。
现在,我们就结合“诗三百”编程语言项目,给大家讲解解释器模式。在讲解之前,首先要打消一些人心中的疑虑。
解释器模式有用吗?解释器模式,主要应用在开发编译器的项目,也就是设计编程语言的项目。这种业务场景比较冷门,一般程序员都比较陌生。大部分程序员,究其整个职业生涯,也往往不能遇到这种项目。虽然作为码农,编译器我们天天都在用,但是不知道编译器的原理,我们过得貌似也很好。但是,如果你了解了编程语言是怎么开发设计的,了解它的运行原理,了解语法的语法,你的好可能会更上一个层次。就像我们小学课本里面学习的古诗“欲穷千里目,更上一层楼”。
业务场景解释器模式,除了开发编译器之外,还有其他几个用途,比如,日志解析,代码格式化,自定义查询语句等。这些业务场景,大家遇到的概率还是比较高的。下面是使用了JavaCC来进行脚本语言开发的开源软件,大家看看对你是否有所启发。
作为架构师,一个很重要的技能,就是技术选型,而技术选型的前提,就是眼界开阔,如果没有足够的知识广度,遇到问题,很容易找不到解决问题的方向。
增加语感程序员,天天和开发语言打交道,和各种语法打交道。如果能从编译器角度,理解开发语言语法的设计原理,对增加我们的语感,是大有益处的。了解编译器的语法规范,就是了解语法的语法。我们才能对我们常用的编程语言的语法,有更深的理解。
加深理解JVM原理学习Java语言,就逃不开学习JVM,这也是面试的重灾区。还有就是编程时,经常发生的堆栈溢出,变量范围冲突等问题,如果对编译器的开发有一点点了解,就会对这些问题,有直观的,形象的认识。因为编写编译器的抽象语法树上各个节点的解释器,主要处理的就是这些内容。
最后,就是我们重点还是放在解释器模式的学习上,对于编译器,有不懂的地方,也不用焦虑,学习要有所侧重。不要钻牛角尖。下来我们进入正题。
现在项目已经有了,我们就单刀直入,看看解释器模式的庐山真面目。
定义解析Given a language define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
—— Gof《Design Patterns: Elements of Reusable Object-Oriented Software》
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
——Gof《设计模式:可复用面向对象软件的基础》
说句实在话,看到解释器模式的定义,我内心是很失望的。这个概念太具体了,毫无想象空间。牢牢锁死了解释器模式的应用场景。
这里面的关键词是语言,表示,解释器。
语言
自不必说,就是我们常说的开发语言,如我刚刚开发的“诗三百”编程语言。
表示(语法的语法)
这里的“表示”,是用来定义“它的文法”,这里的“它”,就是前面所说的“开发语言”,也就是开发语言的文法的一种表示,也就是语法的语法。
语法的语法,前面的“语法”,是程序员需要了解,掌握的语法,后面的“语法”,是编译器要能识别,解析的语法。
比如,在“诗三百”中,我们知道它的变量定义语句,语法格式如下:
变量类型 变量名称;
变量类型有三种,分别是文字之(字符串),整数之(INT),阴阳之(BOOL)。
变量名称也有一定的规范,比如不能用数字开头。
而这些规范和要求,在编译器中是如何定义的呢,编译器是如何识别的呢。如下:
/** Variable declaration. */
void VarDeclaration() :
{ Token t; }
{
(
<BOOL> { jjtThis.type = BOOL; }
|
<INT> { jjtThis.type = INT; }
|
<String> { jjtThis.type = STRING; }
)
t = <IDENTIFIER>
{ jjtThis.name = t.image;}
}
上面的代码,在编译器里面,称为语法规范(Syntactic specification)。
VarDeclaration()代表变量定义的语法规范,它里面包括两个部分,变量类型和变量名称。变量类型是三选一,“诗三百”语言,目前支持三种数据类型。
我用红色标注的部分,就是变量定义语句中的“变量类型”。
我用黄色标注的部分,就是变量定义语句中的“变量名称”。
而变量类型(<BOOL>,<INT>,<STRING>)和变量名称(<IDENTIFIER>)本身的语法规范,又是通过Token来定义的,这个在编译器里面,称为词法规范(Lexical specification)
TOKEN : /* Types */
{
< INT: "\u6574\u6570\u4e4b" > //整数之
|
< BOOL: "\u9634\u9633\u4e4b" > //阴阳之
|
< STRING: "\u6587\u5b57\u4e4b" > //文字之
}
TOKEN :
{
< IDENTIFIER: <LETTER> (<PART_LETTER>)* >
}
上面的词法规范和语法规范,就是变量定义语句,在编译器里面的语法。
举一反三,对于为什么变量名不能以数字开头,大家看看<LETTER>的词法规范,就会明白了。
当遇到一个变量定义语法时,“诗三百”编程语言底层,是如何操作的呢,我们的主角——解释器该上场了。
/* Generated By:JJTree: Do not edit this line. ASTVarDeclaration.java Version 7.0 */
/* JavaCCOptions:MULTI=true NODE_USES_PARSER=false VISITOR=false TRACK_TOKENS=true NODE_PREFIX=AST NODE_extends= NODE_FACTORY= SUPPORT_CLASS_VISIBILITY_PUBLIC=true */
package com.geekarchitect.ssb.parser;
public class ASTVarDeclaration extends SimpleNode implements SSBParserConstants {
int type;
String name;
public ASTVarDeclaration(int id) {
super(id);
}
public ASTVarDeclaration(SSBParser p int id) {
super(p id);
}
@Override
public String toString() {
StringBuffer nodeInfoSb = new StringBuffer();
nodeInfoSb.append(getNodeName()).append("(");
nodeInfoSb.append("type=").append(getTypeTokenName());
nodeInfoSb.append(" name=").append(name);
nodeInfoSb.append(")");
return nodeInfoSb.toString();
}
public String getTypeTokenName() {
String result;
if (type == BOOL)
result = "阴阳之";
else if (type == INT)
result = "整数之";
else {
result = "文字之";
}
return result;
}
public void interpret() {
if (type == BOOL)
symbolTable.put(name new Boolean(false));
else if (type == INT)
symbolTable.put(name new Integer(0));
else {
symbolTable.put(name ""); //todo null
}
}
}
/* JavaCC - OriginalChecksum=fee97198bc98282577622a153423ce48 (do not edit this line) */
类ASTVarDeclaration是JavaCC编译器框架,根据前面的语法规范,自动生成的类,但是默认生成的类,里面基本上没有什么代码,你现在所看到的里面的代码,都需要编译器开发人员自己实现。而这里面,最重要的就是interpret()方法。
“interpret”,我们现在讲解的解释器模式,英文名称就是Interpreter,大家应该能看出两者的关系了吧。
从这个方法里面的代码,你就能对变量定义语句的底层原理,有很深入,直观地理解。并且对各种类型的变量的默认值,有直观的认识。
比如,当用户定义的是一个布尔类型的变量时。
symbolTable.put(name new Boolean(false));
上面的代码表示,编程语言将添加一个变量。默认值是false。symbolTable你可以理解为一个存放变量的池子,数据类型的Map类型。
综上所述,解释器设计模式,就是通过词法规范,语法规范和解释器,来实现一种编程语言。
下面,我使用REIS分析模式,对解释器模式进行分析,总结。
REIS模型分析模板方法模式REIS模型是我总结的分析设计模式的一种方法论,主要包括场景(scene),角色(role),交互(interaction),效果(effect)四个要素。
场景(Scene)场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。
对于解释器模式,它的场景太明确了,从它的概念中就能体现出来。
有解释器的地方,就有一门“语言”,这个语言,可以是强大的,诸如,Java语言,C系列语言,Go语言,Python语言等。也可以是小型的,比如:Sql语言,lua脚本,Lucene框架的查询语言等等。解释器模式,就是用来解释执行编程语言的。
角色(Role)角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。
在解释器模式中,原著中定义了五种角色,如下图所述,分别是抽象表达式(AbstractExpression),终结符表达式(terminalExpression)和非终结符表达式(NonterminalExpression),上下文(Context)和客户方(Client)。
上下文(Context)和客户方(Client),自不必我多说,我们的重点还是要放在,抽象表达式(AbstractExpression),终结符表达式(TerminalExpression)和非终结符表达式(NonterminalExpression),这三个角色上。
这里引出两个术语,Terminal(终结符)和Nonterminal(非终结符),这两个术语,如何理解呢。先从名字上单刀直入,terminal,中文意思终结的,终端的,意味着已经到头了,不能再拆分了,而Nonterminal则反之。
还是太抽象,以大家熟悉的数据结构——“树”结构为例。terminal,就是树结构的叶节点,只能有父节点,不能有子节点,反之,Nonterminal则是分支节点,可以有父节点,也可以有自己的子节点。
还是太抽象,举个栗子。
李白投票数 = 李白投票数 加 投票数
这个语句中,“加”操作符,在AST(Abstract Syntax Tree,抽象语法树)中,对应的节点,就是Nonterminal节点,它里面可以包含左(李白投票数)和右(投票数),两个子节点。
如下代码,ASTAddNode类,代表的就是“加”操作,它里面有两个子节点,红色的是左侧节点,在这个案例里面,就是“李白投票数”,黄色的是右侧节点,代表的就是“投票数”。
/* Generated By:JJTree: Do not edit this line. ASTAddNode.java Version 7.0 */
/* JavaCCOptions:MULTI=true NODE_USES_PARSER=false VISITOR=false TRACK_TOKENS=true NODE_PREFIX=AST NODE_EXTENDS= NODE_FACTORY= SUPPORT_CLASS_VISIBILITY_PUBLIC=true */
package com.geekarchitect.ssb.parser;
public
class ASTAddNode extends SimpleNode {
public ASTAddNode(int id) {
super(id);
}
public ASTAddNode(SSBParser p int id) {
super(p id);
}
public void interpret() {
jjtGetChild(0).interpret();
jjtGetChild(1).interpret();
stack[--top] = new Integer(((Integer) stack[top]).intValue()
((Integer) stack[top 1]).intValue());
}
}
/* JavaCC - OriginalChecksum=acf3473cf87e208c095f2bef11c21aea (do not edit this line) */
而“李白投票数”和“投票数”,在这个表达式中,是变量,它们就属于terminal节点,不能再有自己的子节点。
ASTId代表的就是变量,从它里面的interpret()方法,我们可以看出,当编译器遇到变量时,会把变量从symbolTable中取出来,存放在堆中。
/* Generated By:JJTree: Do not edit this line. ASTId.Java Version 7.0 */
/* JavaCCOptions:MULTI=true NODE_USES_PARSER=false VISITOR=false TRACK_TOKENS=true NODE_PREFIX=AST NODE_EXTENDS= NODE_FACTORY= SUPPORT_CLASS_VISIBILITY_PUBLIC=true */
package com.geekarchitect.ssb.parser;
public
class ASTId extends SimpleNode {
String name;
public ASTId(int id) {
super(id);
}
public ASTId(SSBParser p int id) {
super(p id);
}
public void interpret() {
stack[ top] = symbolTable.get(name);
}
}
/* JavaCC - OriginalChecksum=a5dfd65c85b38212ad8a816c30518ed3 (do not edit this line) */
总结一下,我的观点认为,解释器模式里面的角色,除了上下文(Context)和客户方(Client),我认为抽象表达式(AbstractExpression),终结符表达式(TerminalExpression)和非终结符表达式(NonterminalExpression)这三个角色,在实际开发中,常常以树节点的形式出现的,以便构成Abstract Syntax Tree,抽象语法树,以及在原著中,对于访问者模式的部分讲解,所以,我认为,改名为抽象节点(AbstractNode),终结符节点(TerminalNode)和非终结符节点(NonterminalNode)更确切一些,当然,名字而已,说重要也重要,说不重要,也不重要,毕竟没有改变本质。不过,用已知的,熟悉的知识,理解新的,陌生的知识,往往能达到事半功倍的效果。
交互(interaction)交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。
对于解释器模式中,终结符节点(TerminalNode)和非终结符节点(NonterminalNode)之间的交互,一言以蔽之,就是“递归调用”。
如下的源代码:
文字之 李白;
阅读 : 李白;
倘若 ( 李白 等乎 "李白" ) 云
书写 : "是";
雨
否则 云
书写 : "否";
雨
对应的抽象语法树如下:
运行过程就是:
当我们调用CompilationUnit节点的Interpret方法时,会依次调用它里面的三个子节点(VarDeclaration,ReadStatement,IfStatement)的Interpret方法,这三个子节点,依次调用各自子节点的Interpret方法,直至到达终结符节点为止。
效果(effect)效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。
从我们前面的案例可以看出,解释器模式达到了以下效果。
对于解释器模式,我只说它的缺点,或者是挑战。
1,词法规则和语法规则的定义
要设计一门语言,即使是最简单的脚本语言,要进行词法和语法定义,都是一件不太容易的事情,当然,如果仅仅是定义一个用于运算的表达式,可能直接使用我们熟悉的正则表达式,就可以搞定,而如果是复杂的,需要支持复杂运行机制的语法,比如分支语句,循环语句,异常处理,等等诸如此类,仅仅依靠正则表达式,是一个非常大的挑战。需要需求更简单的方法。
2,抽象语法树的构建
即使我们定义出了语言的词法和语法规则,要解析词法和语法,把用户输入的源代码,构建成面向对象的,由终结符节点和非终结符节点组成的抽象语法树,也不是一件容易的事情。需要大量繁琐的工作。
上面这两个难题,是直接使用解释器模式,不依靠任何框架,会遇到的挑战。这个时候,我们的编译器框架就闪亮登场了。
编译器框架,通俗的说,就是编译器的编译器。业内比较著名的,重量级的编译器框架,就是yacc(Yet Another Compiler Compiler),使用它还需要Lex(Lexical Analyzer Generator)配合使用,前者负责语法分析,后者负责词法分析,主要用在C系列语言。而在Java领域,JavaCC(Java Compiler Compiler)则广受欢迎,生成的编译器,可以运行在JVM虚拟机上面,同时支持词法分析和语法分析功能,比较简洁。
综上所述,基于REIS分析模式,解释器模式有5种角色组成,分别是抽象节点(AbstractNode),终结符节点(TerminalNode),非终结符节点(NonterminalNode)上下文(Context)和客户方(Client)。它的宗旨是,通过解析词法规范和语法规范,构建包含终结符节点和非终结符节点的抽象语法树,来解释执行编程语言。
通用代码和类图对于解释器这种级别的设计模式,通用代码没有任何实际价值,来参考价值都没有,所以,我就提供了。大家如果感兴趣,可以到网上搜搜,借鉴一下。
后续规划解释器模式的内容,我们就讲解到这里。和编译器相关的还有一个很重要的设计模式,就是行为型设计模式中的——访问者模式(Visitor Pattern)。
下期,我们就开始访问者模式的讲解。
极客架构师,专注架构师成长。
关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。