mike-neckのブログ

Java or Groovy or Swift or Golang

型引数からnewしたい時のおすすめの方法をためしてみた

数日前になぎせさんツイッター

型引数からその型のオブジェクトを作りたいときにはSupplierを渡すのがおすすめです

的なことをつぶやいていたので、試しにやってみた。

(どのツイートだったか探してたけど見つからなかったことは秘密です)

【2015/03/11 15:27 追記】

教えてもらいました


@RunWith(Theories.class)なパラメタライズドテストを実行するときに、

複数件あるデータは@DataPointsで渡すわけですが、

これを実行するときに入力値と期待値をひとまとめにしたオブジェクトの配列を

渡したかったりします。

くわしくはいろふさんの記事を参照


例によって、ダラダラ書いているので結論はこちらで、Gistはこちら


いろふさんの記事ではFixtureというクラスを作っていましたが、

パラメタライズドテストが増えてくると、同じようなクラスを何個も作るのが

面倒になってきて、ついには次のような汎用的なクラスを作りたくなってきます。

public class InputAndExpected<IN, EX> {
    private final IN in;
    private final EX ex;
    public InputAndExpected(IN in, EX ex) {
        this.in = in;
        this.ex = ex;
    }
    public IN getInput() {return in;}
    public EX getExpected() {return ex;}
}

さて、これを配列使うのがアレな僕が実際にテストで使おうとすると、

やっかいごとが発生します。

    @DataPoints
    public static InputAndExpected<String, Integer>[] testData() {
        List<InputAndExpected<String, Integer>> list = new ArrayList<>();
        list.add(new InputAndExpected<>("hoge", 4));
        list.add(new InputAndExpected<>("foo", 3));
        return list.toArray();
    }

コンパイルは通るのですが、「操作は未チェックまたは安全ではありません」と警告が出てきます。

これをいい感じにコンパイルを通すために、InputAndExpected<String, Integer>を継承したインナークラスを作ってあげます。

仕方ないので、インナークラスつくります。

    @DataPoints
    @SuppressWarnings("unchecked")
    public static InputAndExpected<String, Integer>[] testData() {
        List<TestData> list = new ArrayList<>();
        //省略
        return list.toArray(new TestData[list.size()]);
    }

    private static class TestData extends InputAndExpected<String, Integer> {
        TestObject(String in, int ex) {
            super(in, ex);
        }
    }

いろふさんが先ほどの記事で最後に書いているように、いちいちlist.add(new TestData("foo", 3));と書かなくてもいいように、when("foo").then(3)のようにテストデータを追加できるような快適メソッドを作りたいと思います(車輪の再発明)。

まずはテストデータを一旦保持するコンテナオブジェクトをつくります。

    private static Generator<TestData> data = Generator.provider(TestData::new);

次にテストデータを追加していく部分を記述します。

    @DataPoints
    public static InputAndExpected<String, Integer> testData =
            data.when("hoge").then(4)
                    .when("foo").then(3)
                    .when("bar").then(3)
                    .toArray();

Generatorなどというインターフェースはないのでつくります。

public interface Generator<OBJ> {
}

when(String)then(Integer)when(String)→…→toArray()という順番で呼び出したいので、

  1. thenだけのメソッドを持つインターフェースをつくります。
  2. Generatorに1.のインターフェースを返すwhenというメソッドを付与します。
  3. GeneratorOBJ[]を返すtoArrayというメソッドを付与します。
public interface Generator<OBJ> {
    Expecting when();
    OBJ[] toArray();
    public static Interface Expecting<O> {
        Generator<O> then();
    }
}

OInputAndExpected<IN, EX>型であるので、その情報を付与します。

public interface Generator<OBJ extends InputAndExpected<IN, EX>> {
    Expecting when();
    OBJ[] toArray();
    public static Interface Expecting<O extends InputAndExpected<I, E>> {
        Generator<O> then();
    }
}
  • whenINを引数に取ります。
  • thenEXを引数に取ります。
  • GeneratorExpectingINEXの型情報が足らないので追加します。
public interface Generator<OBJ extends InputAndExpected<IN, EX>, IN, EX> {
    Expecting<OBJ, IN, EX> when(IN in);
    OBJ[] toArray();
    public static Interface Expecting<O extends InputAndExpected<I, E>, I, E> {
        Generator<O, I, E> then(E ex);
    }
}

Generatorインスタンスを返すstaticメソッドGeneratorに追加します。なお、INEXは別の型ですので、TestData::newBiFunction<I, E, O extends InputAndExpected<I, E>となります。

public interface Generator<OBJ extends InputAndExpected<IN, EX>, IN, EX> {
    public static <O extends InputAndExpected<I, E>, I, E>
    Generator<O, I, E> provider(
            BiFunction<I, E, O> fun) {
        return new GeneratorImpl<>(fun);
    }
    Expecting<OBJ, IN, EX> when(IN in);
    OBJ[] toArray();
    public static Interface Expecting<O extends InputAndExpected<I, E>, I, E> {
        Generator<O, I, E> then(E ex);
    }
}

適当に実装クラスをつくります。

public class GeneratorImpl<OBJ extends InputAndExpected<IN, EX>, IN, EX>
        implements Generator<OBJ, IN, EX> {
    private final BiFunction<IN, EX, OBJ> fun;
    private final List<Triple<OBJ, IN, EX>> list = new ArrayList<>();

    GeneratorImpl(BiFunction<IN, EX, OBJ> fun) {
        this.fun = fun;
    }

    @Override
    public Expecting<OBJ, IN, EX> when(IN in) {
        return ex -> {
            list.add(new Pair<>(function, in, ex));
            return GeneratorImpl.this;
        };
    }

    @Override
    public OBJ[] toArray() {
    }

    private static class Triple<O extends InputAndExpected<I, E>, I, E> {
        private final I in;
        private final E ex;
        private final BiFunction<I, E, O> fun;

        private Triple(BiFunction<I, E, O> fun, I in, E ex) {
            this.fun = fun;
            this.in = in;
            this.ex = ex;
        }

        private O toObject() {
            return fun.apply(in, ex);
        }
    }
}

最後のtoArrayですが、Listのサイズから配列を作って、追加していってもよいのですが、せっかくなのでStreamを使います。

    @Override
    public OBJ[] toArray() {
        return list.stream.map(Triple::toObject).toArray();
    }

これでは型が合わないので、

IntFunction<OBJ[]>であるOBJ[]::newをしたい

ところですが、残念ながら型引数からnewすることはできません。そこで、コンストラクターIntFunction<OBJ[]>を渡すようにします。

    private final IntFunction<OBJ[]> af;
    GeneratorImpl(BiFunction<IN, EX, OBJ> fun, IntFunction<OBJ[]> af) {
        this.fun = fun;
        this.af = af;
    }
    @Override
    public OBJ[] toArray() {
        return list.stream.map(Triple::toObject).toArray(af);
    }

これで、無事にOBJ[]::newっぽいことができました。

あとは、Generatorインターフェースにも反映させます。

    public static <O extends InputAndExpected<I, E>, I, E>
    Generator<O, I, E> provider(
            BiFunction<I, E, O> fun, IntFunction<O[]> af) {
        return new GeneratorImpl<>(fun, af);
    }

テストコードはこんな感じになります。

@RunWith(Theories.class)
public static class SampleTest {
    @DataPoints
    public static InputAndExpected<String, Integer>[] TEST_DATA =
            Generator.provider(TestData::new, TestData[]::new)
                    .when("hoge").then(4)
                    .when("foo").then(3)
                    .when("bar").then(3)
                    .toArray();

    public void test(InputAndExpected<String, Integer> data) {
        assertThat(data.getInput().length(), is(data.getExpected()));
    }

    private static class TestData extends InputAndExpected<String, Integer> {
        protected TestData(String s, Integer i) {
            super(s, i);
        }
    }
}

結構すっきりしたコードになったと自己満足にふけることができます。

型引数からnewしたい時のおすすめの方法をためしてみた see http://mike-neck.hatenadiary.com/entry/2015/03/11/151938

結論

Spock使ったほうが楽だったかも(´・ω・`)