😸

【No.6】字句解析器のリファクタリング(その2)

2023/02/16に公開

トークンの種類で分ける

No.5の記事で挙げた問題点には、以下のようなものがありました。

  1. 構文解析器にて、メソッドでトークンの種類を判別できるようにしたい。
    そのため、字句解析器にてトークンの種類を区別しておく。
    (現在は入力されたコードを空白で分解しただけ。)

現在は、トークンを単なる文字列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);
}

課題

必ず対応すべき課題は、以下のようなもです。

  1. Token抽象クラスのメソッドが、ひど過ぎる
    (デフォルト値を返すのは論外なので、例外のキャストに変更します)
  2. Analyzerクラスのcategorizedメソッドが長すぎ

これらは、のちのち対応していきます。

また、テスト駆動の活用も進んでいません。
今回のようなリファクタにテストは必須なので、もちろんテストは作っています。
(テストに何度も救われました)
ですが、テスト駆動っぽいことは出来ていません。
これが最大の課題ですね。

Discussion