問題の説明
最初の行の列を記述する柔軟な csv のような形式のパーサーをリファクタリングしたいと考えています。この情報に応じて、単純な属性だけでなく、a (スペースで区切られた) などの複雑な属性を持つオブジェクトをパーサーに構築させたいと考えています。List<String>
たとえば、Thing
s:
データ型の例
import java.util.List;
public class Thing {
protected int foo;
protected String bar
protected List<String> baz;
public Thing(int foo, String bar, List<String> baz) {
this.foo = foo;
this.bar = bar;
this.baz = baz;
}
public String toString() {
return "foo: " + foo + ", bar: " + bar + ", baz: " + baz;
}
}
パーサーの入力は、最初の行に列行 (カンマ区切り) があり、n
次の行 (カンマ区切り) にデータがあるテキスト ファイルです。テストを簡単にするためIterator<String>
に、入力行に使用します。この簡単なテストは、私が構築したいものを示しているはずです:
JUnit テスト
// prepare example string iterator
List<String> lines = new ArrayList<String>();
lines.add("bar,baz,foo");
lines.add("yay,quux quuux,17");
lines.add("hey,qaax qaaax,42");
// test parsed things
List<Thing> things = ThingBuilder.buildThings(lines.iterator());
assertNotNull(things);
assertEquals(2, things.size());
assertEquals("foo: 17, bar: yay, baz: [quux, quuux]", things.get(0).toString());
assertEquals("foo: 42, bar: hey, baz: [qaax, qaaax]", things.get(1).toString());
最も簡単なアプローチ
- 最初の行を読み取り、列名に分割します
- 他のすべての行を読み、それらに対して次の操作を行います。
- 行をトークンに分割する
- それらをループします:
- トークンの場合、列名に
i
大きなswitch
/を実行しますelse if
i
- 変換トークン
i
- 抽出された値をどこかに保存する
- トークンの場合、列名に
- すべてを集めて構築する
Thing
- 終わり。
このアプローチに関する私の問題は、内側のスイッチです。最初の行を処理すると、行がどのように解析されるかが明確になります。
私が欲しいもの
クロージャーのある言語では、次のことを試します。
- 最初の行を読み取り、列名に分割します
- 各列名に対して、特定のトークンに適切な値を設定するクロージャーを作成し、それをパーサークロージャーの配列に追加します
- 他のすべての行を読み、それらに対して次の操作を行います。
- 行をトークンに分割する
- それらをループします:
i
トークンを使用してパーサー クロージャーを呼び出すi
- すべてを集めて構築する
Thing
- 終わり。
私が試したこと
3 つのトークン パーサーすべてに対応するシンプルなインターフェイスがあります。それらはトークンを取得し、生成された値を指定されたThingBuilder
のキャッシュに挿入することになっています。
public interface TokenParser {
public void parse(String token, ThingBuilder builder);
}
public class FooParser implements TokenParser {
@Override public void parse(String token, ThingBuilder builder) {
builder.setFoo(Integer.parseInt(token));
}
}
public class BarParser implements TokenParser {
@Override public void parse(String token, ThingBuilder builder) {
builder.setBar(token);
}
}
import java.util.ArrayList;
import java.util.List;
public class BazParser implements TokenParser {
@Override public void parse(String token, ThingBuilder builder) {
List<String> baz = new ArrayList<String>();
for (String s : token.split(" ")) baz.add(s);
builder.setBaz(baz);
}
}
MyThingBuilder
のbuildThings
メソッドは静的で、ThingBuilder
内部でオブジェクトを作成します。コンストラクターは最初の (列) 行を取得します。これは、トークン パーサー リストが入力される場所でもあります。この後、隠しThingBuilder
オブジェクトの準備が整い、次の入力行でbuildThing
メソッドが繰り返し呼び出されて のリストが作成されますThing
。
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
public class ThingBuilder {
// single column parsers
protected List<TokenParser> columnParsers;
// thing attribute cache
protected int fooCache;
protected String barCache;
protected List<String> bazCache;
// thing attribute cache setter
public void setFoo(int foo) { fooCache = foo; }
public void setBar(String bar) { barCache = bar; }
public void setBaz(List<String> baz) { bazCache = baz; }
// cleanup helper method
protected void cleanup() {
setFoo(0); setBar(null); setBaz(null);
}
// statically build a list of things from given lines
public static List<Thing> buildThings(Iterator<String> lines) {
// prepare builder with the first line
ThingBuilder builder = new ThingBuilder(lines.next());
// parse things
List<Thing> things = new ArrayList<Thing>();
while (lines.hasNext()) {
things.add(builder.buildThing(lines.next()));
}
return things;
}
// prepares a builder to parse thing lines
protected ThingBuilder(String columnLine) {
// split line into columns
String[] columns = columnLine.split(",");
// prepare a parser for each column
columnParsers = new ArrayList<TokenParser>();
for (String column : columns) {
TokenParser parser;
if (column.equals("foo")) parser = new FooParser();
else if (column.equals("bar")) parser = new BarParser();
else if (column.equals("baz")) parser = new BazParser();
else throw new RuntimeException("unknown column: " + column);
columnParsers.add(parser);
}
}
// builds a thing from a string
protected Thing buildThing(String line) {
// split the line in tokens
String[] tokens = line.split(",");
// let the parsers do the work
for (int i = 0; i < tokens.length; i++) {
columnParsers.get(i).parse(tokens[i], this);
}
// hopefully they're done
Thing thing = new Thing(fooCache, barCache, bazCache);
cleanup();
return thing;
}
}
これは機能しますが、次のとおりです。
ソリューションの気に入らない点
- 複雑な気分!
- パブリック キャッシュ セッターのこと。sのみ
TokenParser
がビルダー キャッシュを満たすことができるようにする必要があります。 - の付いた列が複数ある場合はどうすればよい
int
ですか? 列ごとに 1 つのパーサー クラスを作成する必要がありますか、それとも IntegerParser クラスを複数回使用することは可能ですか? ここでの問題は、パーサーが正しいキャッシュ セッター メソッドを呼び出さなければならないことです。
ヒントをお寄せいただきありがとうございます。