快捷搜索:  汽车  科技

编程学习哪些开发工具比较好(学了这个设计模式)

编程学习哪些开发工具比较好(学了这个设计模式)程序员,天天和开发语言打交道,和各种语法打交道。如果能从编译器角度,理解开发语言语法的设计原理,对增加我们的语感,是大有益处的。了解编译器的语法规范,就是了解语法的语法。我们才能对我们常用的编程语言的语法,有更深的理解。作为架构师,一个很重要的技能,就是技术选型,而技术选型的前提,就是眼界开阔,如果没有足够的知识广度,遇到问题,很容易找不到解决问题的方向。现在,我们就结合“诗三百”编程语言项目,给大家讲解解释器模式。在讲解之前,首先要打消一些人心中的疑虑。解释器模式,主要应用在开发编译器的项目,也就是设计编程语言的项目。这种业务场景比较冷门,一般程序员都比较陌生。大部分程序员,究其整个职业生涯,也往往不能遇到这种项目。虽然作为码农,编译器我们天天都在用,但是不知道编译器的原理,我们过得貌似也很好。但是,如果你了解了编程语言是怎么开发设计的,了解它的运行原理,了解语法的语法,你的好可能会更上

学了这个设计模式,就可以开发一个自己的编程语言玩玩;学了这个设计模式 JVM不再那么深不可测;学了这个设计模式,编译器,So easy;为了分享这个设计模式,码农老吴,给自己挖了一个深不见底的坑。


编程学习哪些开发工具比较好(学了这个设计模式)(1)

大家好,欢迎关注极客架构师,极客架构师——专注架构师成长,我是码农老吴。

本节是《架构师基本功之设计模式》第13期-第2节

在第一节,我给大家演示了,我刚刚开发的,一门运行于JVM虚拟机之上,蕴含中国传统文化基因,顺应中文写作习惯,优美雅致、韵味悠长的编程语言——诗三百,之所以开发设计这个编程语言,就是为了给大家更好的讲解,行为型设计模式中的——解释器模式。

现在,我们就结合“诗三百”编程语言项目,给大家讲解解释器模式。在讲解之前,首先要打消一些人心中的疑虑。

解释器模式有用吗?

解释器模式,主要应用在开发编译器的项目,也就是设计编程语言的项目。这种业务场景比较冷门,一般程序员都比较陌生。大部分程序员,究其整个职业生涯,也往往不能遇到这种项目。虽然作为码农,编译器我们天天都在用,但是不知道编译器的原理,我们过得貌似也很好。但是,如果你了解了编程语言是怎么开发设计的,了解它的运行原理,了解语法的语法,你的好可能会更上一个层次。就像我们小学课本里面学习的古诗“欲穷千里目,更上一层楼”。

业务场景

解释器模式,除了开发编译器之外,还有其他几个用途,比如,日志解析,代码格式化,自定义查询语句等。这些业务场景,大家遇到的概率还是比较高的。下面是使用了JavaCC来进行脚本语言开发的开源软件,大家看看对你是否有所启发。

作为架构师,一个很重要的技能,就是技术选型,而技术选型的前提,就是眼界开阔,如果没有足够的知识广度,遇到问题,很容易找不到解决问题的方向。

编程学习哪些开发工具比较好(学了这个设计模式)(2)

增加语感

程序员,天天和开发语言打交道,和各种语法打交道。如果能从编译器角度,理解开发语言语法的设计原理,对增加我们的语感,是大有益处的。了解编译器的语法规范,就是了解语法的语法。我们才能对我们常用的编程语言的语法,有更深的理解。

加深理解JVM原理

学习Java语言,就逃不开学习JVM,这也是面试的重灾区。还有就是编程时,经常发生的堆栈溢出,变量范围冲突等问题,如果对编译器的开发有一点点了解,就会对这些问题,有直观的,形象的认识。因为编写编译器的抽象语法树上各个节点的解释器,主要处理的就是这些内容。

最后,就是我们重点还是放在解释器模式的学习上,对于编译器,有不懂的地方,也不用焦虑,学习要有所侧重。不要钻牛角尖。下来我们进入正题。

现在项目已经有了,我们就单刀直入,看看解释器模式的庐山真面目。

编程学习哪些开发工具比较好(学了这个设计模式)(3)

定义解析

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)。

编程学习哪些开发工具比较好(学了这个设计模式)(4)

上下文(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)之间的交互,一言以蔽之,就是“递归调用”。

如下的源代码:

文字之 李白; 阅读 : 李白; 倘若 ( 李白 等乎 "李白" ) 云 书写 : "是"; 雨 否则 云 书写 : "否"; 雨

对应的抽象语法树如下:

编程学习哪些开发工具比较好(学了这个设计模式)(5)

运行过程就是:

当我们调用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)。

编程学习哪些开发工具比较好(学了这个设计模式)(6)

下期,我们就开始访问者模式的讲解。

极客架构师,专注架构师成长。

关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。

猜您喜欢: