2

問題の説明

最初の行の列を記述する柔軟な csv のような形式のパーサーをリファクタリングしたいと考えています。この情報に応じて、単純な属性だけでなく、a (スペースで区切られた) などの複雑な属性を持つオブジェクトをパーサーに構築させたいと考えています。List<String>たとえば、Things:

データ型の例

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());

最も簡単なアプローチ

  1. 最初の行を読み取り、列名に分割します
  2. 他のすべての行を読み、それらに対して次の操作を行います。
    • 行をトークンに分割する
    • それらをループします:
      • トークンの場合、列名にi大きなswitch/を実行しますelse ifi
      • 変換トークンi
      • 抽出された値をどこかに保存する
    • すべてを集めて構築するThing
  3. 終わり。

このアプローチに関する私の問題は、内側のスイッチです。最初の行を処理すると、行がどのように解析されるかが明確になります。

私が欲しいもの

クロージャーのある言語では、次のことを試します。

  1. 最初の行を読み取り、列名に分割します
  2. 各列名に対して、特定のトークンに適切な値を設定するクロージャーを作成し、それをパーサークロージャーの配列に追加します
  3. 他のすべての行を読み、それらに対して次の操作を行います。
    • 行をトークンに分割する
    • それらをループします:
      • iトークンを使用してパーサー クロージャーを呼び出すi
    • すべてを集めて構築するThing
  4. 終わり。

私が試したこと

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);
    }
}

MyThingBuilderbuildThingsメソッドは静的で、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 クラスを複数回使用することは可能ですか? ここでの問題は、パーサーが正しいキャッシュ セッター メソッドを呼び出さなければならないことです。

ヒントをお寄せいただきありがとうございます。

4

2 に答える 2

2

個人的には、私はこの道をたどりません。この種のことを非常にうまく行い、カスタマイズ可能な拡張ポイントを可能にする一般的なフレームワークがあります。

たとえば、人気のあるOpenCSVプロジェクトを考えてみましょう。

http://opencsv.sourceforge.net/#javabean-integration

または、注釈が必要な場合は、JFileHelpersを検討してください

http://jfilehelpers.com/index_en.php

またはJSefa

http://jsefa.sourceforge.net/quick-tutorial.html#CSV

于 2012-10-14T00:38:58.057 に答える
1

@btiernayの答えに同意しますが、独自の実装を展開したい場合は、読み進めてください...


パブリック キャッシュ セッターのこと。ビルダー キャッシュを埋めるには、TokenParsers のみを許可する必要があります。

ええ。これはTokenParserAPI の結果であり、 でセッターを呼び出して値を「返す」方法ですThingBuilder。実際、これはあなたが特定したものよりもさらに悪い結果をもたらします. つまり、TokenParserAPI とすべてのTokenParserクラスは、1 つのThingBuilderクラスだけに固有です。それらは再利用できません...

次のような API を使用したほうがよいと思います。

  public interface TokenParser<T> {
      public T parse(String token);
  }

int を持つ列が複数ある場合はどうなりますか? 列ごとに 1 つのパーサー クラスを作成する必要がありますか、それとも IntegerParser クラスを複数回使用することは可能ですか? ここでの問題は、パーサーが正しいキャッシュ セッター メソッドを呼び出さなければならないことです。

うん。

次に、RowBuilder インターフェイスを作成します。

  public interface RowBuilder<R>
      public R buildRow(List<String> tokens);
  }

そして、ここにトリッキーなビットがあります --- 次のような一般的な RowBuilder クラスを作成します。

  public class GenericRowBuilder<R> implements RowBuilder<R> {
      public GenericRowBuilder(Class<R> clazz, TokenParser<?>[] parsers) {
          // Extract the return types of the reified parse objects' `parse` 
          // methods, and use this to locate a matching `Constructor<R>` in 
          // `clazz`.  If there isn't one, throw an exception.
          this.clazz = clazz;
          this.parsers = parsers;
      }
      public R parse(List<String> tokens) {
          // Check number of tokens matches number of parsers.
          // Parse each token with corresponding parsers.
          // Use the `Constructor<R>` found above to create the instance of `R`
      }
  }

これはすべてかなり複雑です... Java のリフレクション API を使用するには十分な理解が必要ですが、最終的には、次のようにクラスの RowBuilder をインスタンス化できます。

  RowBuilder<MyRow> rb = new GenericRowBuilder<MyRow>(MyRow.class,
      new TokenParser<?>[]{
          new IntTokenParser(), new FloatTokenParser(), new CustomTokenParser});

そして、あなたは何かを持っています:

  • 正しいフィールドに正しいタイプが使用されていることを確認し、
  • 各行の値の数を確認し、各列の値に適切なパーサーを使用します。
  • R適切なコンストラクターがある場合、任意の行クラスで動作します。
于 2012-10-14T00:49:55.253 に答える