😸
【No.6】字句解析器のリファクタリング(その2)
トークンの種類で分ける
No.5の記事で挙げた問題点には、以下のようなものがありました。
- 構文解析器にて、メソッドでトークンの種類を判別できるようにしたい。
そのため、字句解析器にてトークンの種類を区別しておく。
(現在は入力されたコードを空白で分解しただけ。)
現在は、トークンを単なる文字列Stringとして扱っていました。
これだと、トークンの種類の判別が煩雑そうです。
よって、トークンは、Tokenクラスのサブクラスとします。
この記事で作っている言語のBNFは、以下のようなものでした。
<program> ::= PROGRAM <command list>
<command list> ::= <command>* END
<command> ::= <repeat command> | <primitive command>
<repeat command> ::= REPEAT <number> <command list>
<primitive command> ::= GO | RIGHT | LRFT
(結城浩『Java言語で学ぶデザインパターン入門 第3版』を参考に作成。プログラムを大文字化した)
BNFを参考に、ざっとトークンの種類を分けます。
PROGRAM ・・・StartProgramToken
END・・・EndProgramToken
REPEAT・・・RepeatToken
GO,RIGHT,LRFT・・・PrimitiveCommandToken
数字・・・NumberToken
それでは、実装していきます。
実装
Token抽象クラス
各種トークンのスーパークラスとなるのが、Token抽象クラスです。
手抜き工事が丸見えですが、あとで必ずリファクタしますので問題なしです。
Token.java
package lexer;
public abstract class Token {
public String getContents() {return "なし";}
public int getNumber() {return 0;}
//トークンの種類を判別
public boolean isNumberToken() {return false;}
public boolean isPrimitiveCommandToken(){return false;}
public boolean isRepeatToken() {return false;}
public boolean isEndProgramToken() {return false;}
public boolean isStartProgramToken() {return false;}
}
各種トークンのクラス
StartProgramToken.java
package lexer;
public class StartProgramToken extends Token{
@Override
public boolean isStartProgramToken(){return true;}
}
EndProgramToken.java
package lexer;
public class EndProgramToken extends Token{
@Override
public boolean isEndProgramToken(){return true;}
}
RepeatToken.java
package lexer;
public class RepeatToken extends Token{
@Override
public boolean isRepeatToken(){return true;}
}
PrimitiveCommandToken.java
package lexer;
public class PrimitiveCommandToken extends Token{
private String contents;
public PrimitiveCommandToken(String code) {
this.contents = code;
}
@Override
public boolean isPrimitiveCommandToken(){return true;}
@Override
public String getContents() {return contents;}
}
NumberToken.java
package lexer;
public class NumberToken extends Token{
private int number;
public NumberToken(String code) {
number = Integer.parseInt(code);
}
@Override
public boolean isNumberToken() {return true;}
@Override
public int getNumber(){return number;}
}
字句解析器
字句解析器の実装も変更しました。
トークン列を保持し、要求があればトークンを提供するクラス
Lexer.java
package lexer;
import java.util.*;
public class Lexer{
private List<Token> categorizedTokens = new ArrayList<>();
private int index = 0;
public Lexer(String sourceCode){
analyze(sourceCode);
}
private void analyze(String sourceCode) {
categorizedTokens = new Analyzer(sourceCode).getTokens();
}
public Token getToken() {
return categorizedTokens.get(index);
}
public void nextToken() {
index ++ ;
}
}
ソースコードをトークン列に変換するクラス
ソースコードを空白で区分する処理は残しました。
空白で区分したのち、トークンの種類ごとに分類します。
ちょっと、categorizedメソッドが長すぎますね。
Analyzer.java
package lexer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Analyzer{
private String[] tokens;
private List<Token> categorizedTokens = new ArrayList<>();
Analyzer(String sourceCode){
separate(sourceCode);
categorized();
}
List<Token> getTokens(){return categorizedTokens;}
private void separate(String sourceCode){
tokens =sourceCode.split("\\s+");
}
private void categorized() {
for(int i=0;i<tokens.length;i++) {
String token = tokens[i];
Pattern primitivePattern = Pattern.compile("(GO|RIGHT|LEFT)");
Matcher primitiveMach = primitivePattern.matcher(token);
if(primitiveMach.find()) {
PrimitiveCommandToken pc =new PrimitiveCommandToken(token);
categorizedTokens.add(pc);
continue;
}
Pattern numberPattern = Pattern.compile("[0-9]+");
Matcher numberMach = numberPattern.matcher(token);
if(numberMach.find()) {
NumberToken pc =new NumberToken(token);
categorizedTokens.add(pc);
continue;
}
Pattern repeatPattern = Pattern.compile("REPEAT");
Matcher repeatMach = repeatPattern.matcher(token);
if(repeatMach.find()) {
RepeatToken pc =new RepeatToken();
categorizedTokens.add(pc);
continue;
}
Pattern endPattern = Pattern.compile("END");
Matcher endMach = endPattern.matcher(token);
if(endMach.find()) {
EndProgramToken pc =new EndProgramToken();
categorizedTokens.add(pc);
continue;
}
Pattern startPattern = Pattern.compile("PROGRAM");
Matcher startMach = startPattern.matcher(token);
if(startMach.find()) {
StartProgramToken pc =new StartProgramToken();
categorizedTokens.add(pc);
continue;
}
}
}
}
構文解析器
構文解析器も変更したのですが、今回は一部分だけ紹介します。
旧
Command.java
if(lexer.getToken().equals("REPEAT")) {
child = new RepeatCommand().parse(lexer);
}else if(lexer.getToken().equals("GO")|
lexer.getToken().equals("RIGHT")|
lexer.getToken().equals("LEFT") ) {
child = new PrimitiveCommand().parse(lexer);
}
新
ちょっと短くなりましたね。可読性は、上がってないような気がします。
Command.java
if(lexer.getToken().isRepeatToken()) {
child = new RepeatCommand().parse(lexer);
}
else if(lexer.getToken().isPrimitiveCommandToken()) {
child = new PrimitiveCommand().parse(lexer);
}
課題
必ず対応すべき課題は、以下のようなもです。
- Token抽象クラスのメソッドが、ひど過ぎる
(デフォルト値を返すのは論外なので、例外のキャストに変更します) - Analyzerクラスのcategorizedメソッドが長すぎ
これらは、のちのち対応していきます。
また、テスト駆動の活用も進んでいません。
今回のようなリファクタにテストは必須なので、もちろんテストは作っています。
(テストに何度も救われました)
ですが、テスト駆動っぽいことは出来ていません。
これが最大の課題ですね。
Discussion