S-JIS[2013-12-16/2017-04-25] 変更履歴

Asakusa Framework Resultクラス

Asakusa FrameworkOperator DSLで使うResultクラスについて。


概要

一部のユーザー演算子では、演算子メソッドの引数にResultクラスを使用する。

AsakusaFWのResultクラスは、演算子の出力ポートを表現しているクラス。
Resultにデータモデル(のオブジェクト)をaddすると、データを出力することになる。

Resultを使った演算子メソッドのテストをするときは、MockResultクラスを使う。
AsakusaFW 0.9.1以降では、OperatorTestEnvironmentのnewResultメソッドを使ってMockResultインスタンスを生成するのが便利。[2017-04-25]


ExampleOperator.java(Operator DSL):

import com.asakusafw.runtime.core.Result;
import com.asakusafw.vocabulary.operator.Extract;

import com.example.modelgen.dmdl.model.Foo;
import com.example.modelgen.dmdl.model.Hoge;
public abstract class ExampleOperator {

	private final Hoge hoge = new Hoge();

	/**
	 * Fooに含まれるHogeを出力する
	 * 
	 * @param foo
	 *         抽出対象のデータモデル
	 * @param out
	 *         hogeの出力
	 */
	@Extract
	public void extractFields(Foo foo, Result<Hoge> out) {
		hoge.reset(); //初期化
		hoge.setKey(foo.getKey());
		hoge.setValue(foo.getValue1());
		out.add(hoge);

		hoge.reset(); //初期化
		hoge.setKey(foo.getKey());
		hoge.setValue(foo.getValue2());
		out.add(hoge);

		hoge.reset(); //初期化
		hoge.setKey(foo.getKey());
		hoge.setValue(foo.getValue3());
		out.add(hoge);
	}
}

Resultの直後には「<データモデルクラス名>」を付けて、出力対象のデータモデルを指定する。

Resultには、「add」というメソッド名が示す通り、複数のデータを出力することが出来る。
java.util.Listがaddメソッドで複数のオブジェクトを追加できるのと同様)


モデルオブジェクトの初期化の必要性

Resultにaddした後では、addしたモデルオブジェクトの内容は破壊される(可能性がある)。[2014-12-21]
(自分がセットしなかったプロパティーに値が入ることもある)
したがって、モデルオブジェクトを使い回している場合は、毎回初期化しておく必要がある。

		result.reset();      	// 全プロパティーを初期化
あるいは
		result.copyFrom(src);	// 他のデータモデルオブジェクトから全プロパティーをコピー

破壊されるというのは、AsakusaFWが最適化の一環で複数の演算子を連続で呼び出すようにすることがある為。
Resultにaddしたオブジェクトがファイルに出力されず、次の演算子に直接渡されたりする。
その演算子でオブジェクトを変更していると、前の演算子で行った以外の変更が行われることになる。

※これは、自分でモデルオブジェクトを作って返す形式の演算子(変換演算子Convert)でも同様。

ちなみに、resetメソッドはnullフラグを立てるだけ(なので非常に軽量)なので、インスタンスを生成し直すより速いかも。


テストの例

このメソッドのテストケースは以下のようになる。

ExampleOperatorTest.java:

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Test;

import com.asakusafw.runtime.testing.MockResult;

import com.example.modelgen.dmdl.model.Foo;
import com.example.modelgen.dmdl.model.Hoge;
	@Test
	public void extractFields() {
		ExampleOperator operator = new ExampleOperatorImpl();

		Foo foo = new Foo();
		foo.setKeyAsString("abc");
		foo.setValue1(123);
		foo.setValue2(456);
		foo.setValue3(789);

		MockResult<Hoge> out = new MockResult<Hoge>() {
			@Override
			protected Hoge bless(Hoge result) {
				Hoge copy = new Hoge();
				copy.copyFrom(result);
				return copy;
			}
		};
//		MockResult<Hoge> out = resource.newResult(Hoge.class); // AsakusaFW 0.9.1

		operator.extractFields(foo, out);

		List<Hoge> result = out.getResults();
		assertThat(result.size(), is(3));

		Hoge hoge0 = result.get(0);
		assertThat(hoge0.getKeyAsString(), is("abc"));
		assertThat(hoge0.getValue(), is(123));
		Hoge hoge1 = result.get(1);
		assertThat(hoge1.getKeyAsString(), is("abc"));
		assertThat(hoge1.getValue(), is(456));
		Hoge hoge2 = result.get(2);
		assertThat(hoge2.getKeyAsString(), is("abc"));
		assertThat(hoge2.getValue(), is(789));
	}

テストクラスでは、Resultを受け取る引数にMockResultを渡す。
MockResultはaddされたデータを内部でListに保持する。
MockResultのgetResultsメソッドでそのListを取得することが出来るので、それを使ってテスト結果の検証を行う。

ただし、デフォルトではaddに渡されたオブジェクトをそのまま内部のListに追加していくので、呼び出し元がオブジェクトを使い回している場合は、追加したつもりのデータが全部同じ(最後に更新された状態)になってしまう。
この問題を解消するために、MockResultにはblessというメソッドが用意されている。

MockResultでは、addに渡されたオブジェクトを引数としてblessメソッドを呼び出す。
そして、blessから返されたオブジェクトを内部のListに保持する。(デフォルトでは、blessメソッドは引数のオブジェクトをそのまま返す)
そこで、blessメソッドをオーバーライドして、内部でオブジェクトのコピーを作って返すようにすれば、呼び出し元でオブジェクトが使い回されても関係なくなる。
※AsakusaFW 0.9.1で、こういった処理を行うMockResultを生成する方法が用意された。→OperatorTestEnvironment.newResult [2017-04-25]


自作リフレクションMockResult

Resultを使うデータモデル毎に個別にMockResultのblessメソッドを実装するのは少々面倒なので、リフレクションでも使ってオブジェクトをコピーしても良かったんじゃないかなーと思わなくもない。
リフレクションを使うと実行速度は多少落ちるが、どうせテストでしか使わないので、基本的には問題ない気がするし。

と思ったが、これくらいならAsakusaFW本体に用意してもらわなくても、自分で作るのは難しくない(笑)
(AsakusaFW 0.9.1で用意されたw →OperatorTestEnvironment.newResult [2017-04-25]

import com.asakusafw.runtime.model.DataModel;
import com.asakusafw.runtime.testing.MockResult;
public class ReflectionMockResult<T extends DataModel<T>> extends MockResult<T> {

	@Override
	protected T bless(T result) {
		if (result == null) {
			return null;
		}

		try {
			@SuppressWarnings("unchecked")
			T copy = (T) result.getClass().newInstance();
			copy.copyFrom(result);
			return copy;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

このReflectionMockResultは以下のようにして使う。

		MockResult<Hoge> out = new ReflectionMockResult<Hoge>();

newResult

AsakusaFW 0.9.1で、OperatorTestEnvironmentクラスにnewResultというメソッドが用意された。[2017-04-25]
newResultによって作られるMockResultは、中で(リフレクションを使って)データモデルをコピーして保持する。

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.List;

import org.junit.Rule;
import org.junit.Test;

import com.asakusafw.runtime.testing.MockResult;
import com.asakusafw.testdriver.OperatorTestEnvironment;
import com.example.modelgen.dmdl.model.Foo;
import com.example.modelgen.dmdl.model.Hoge;
public class ExampleOperatorTest {

	@Rule
	public OperatorTestEnvironment resource = new OperatorTestEnvironment();
	@Test
	public void extractFields() {
		ExampleOperator operator = new ExampleOperatorImpl();

		Foo foo = new Foo();
		foo.setKeyAsString("abc");
		foo.setValue1(123);
		foo.setValue2(456);
		foo.setValue3(789);

		MockResult<Hoge> out = resource.newResult(Hoge.class);

		operator.extractFields(foo, out);

		List<Hoge> result = out.getResults();
		assertThat(result.size(), is(3));

		Hoge hoge0 = result.get(0);
		assertThat(hoge0.getKeyAsString(), is("abc"));
		assertThat(hoge0.getValue(), is(123));
		Hoge hoge1 = result.get(1);
		assertThat(hoge1.getKeyAsString(), is("abc"));
		assertThat(hoge1.getValue(), is(456));
		Hoge hoge2 = result.get(2);
		assertThat(hoge2.getKeyAsString(), is("abc"));
		assertThat(hoge2.getValue(), is(789));
	}
}

Operator DSLへ戻る / AsakusaFW目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま