山崎@東工大です。
引用の順番を若干変えています。
> > 一連の処理を分割した例でこのことを確かめてみますと、、、
> >
> > 話を単純にするために処理を2つに分割することにします。クラスとしては
> > Preprocessor と Postprocessor、中間結果を表す Intermediary、そして テ
> > ストケース PreprocessorTest と PostprocessorTest があります。当然のこ
> > とながら Intermediary 以外のクラスは Intermediary に依存します。特にテ
> > ストケースは、記述の方法が Intermediary に強く依存し、他のクラスと比べ
> > て記述量が増加する可能性が高いわけですから、Intermediary を変更すると、
> > テストケースを大幅に変更しなければならないということが言えるわけです。
> > 後から考えれば非常に当たり前な結論ですね。
>
> そうですね。
> 以下のような前提で拝見します。
> ・Intermediaryはエンティティ(データ)クラス
> ・mainのProcessorはない(PostprocessorはmainのProcessor)
>
> 前半の処理(Preprocessor)と後半の処理(Postprocessor)は、それぞれ
> ある入力から中間結果(Intermediary)を生成し、これを元にある出力を行う
> ものと理解しました。
> # 山崎さんの考えられてるものと合ってますでしょうか?
はい。
> こういった場合には、中間結果が変更されると、それに(おそらく、get/setを
> 介しても、データ構造に)依存したクラス、およびテストも変更を余儀なくされ
> てしまいますよね。
> # よくあるケースですね。
その点を悩んでおりました。
> こういうケースは良くありますし、これで悪くないと思いますが、
> 個人的にはエンティティ同士で(できれば相互に)変換出来る方が好みです。
>
> 以下、(特に良くなっているわけではありませんが)こんな感じですが…、
> # 余計なInput,Outputもデータクラスとしてとりあえず出しておきます。
>
> Input --> Intermediary --> Output
>
> コードは以下みたいな感じで書きたいです。
> // Input ⇒ (Intermediary ⇒) Output
> Input input;
> Intermediary inter = Input.toIntermediary();
> Output output = Inter.toOutput()
Intermediary inter = input.toIntermediary();
Output output = inter.toOutput();
ですよね?
> 理由は、機能クラスを必要以上に登場させたくないのです。
> 例えばユースケースのシーケンスの実行手順を司るようなControllerの場合、機
> 能クラスであっても全然構わないと思うのですが、(Preprocessor)と
> (Postprocessor)については、データに依存した機能クラスになるからです。
> # データに依存したクラスは上記の問題に嵌るからです。
>
> データ間の依存はしかるべきと割り切ります。
> データの変更があれば、依存する変換部分は当然修正されるものと考えます。
なるほど。機能クラスではなくエンティティクラスを前面に出す構成にするわ
けですね。
Input -> Intermediary -> Output のようにクラスが変化するならばこれで良
いと思うのですが、同一のクラスに、データが蓄積されたり、内部データが変
換されたりする場合には、どのようにするのが良いとお考えですか?
> テストはInput、Intermediary、Outputをまとめた単位で、
>
> // input ⇒ intermediary 変換のテスト(逆変換で検証).
> inter = input. toIntermediary();
> atctual = inter.backtoInput();
> assertEquals(inter, actual);
>
> という感じで使いたいところです。
> # それぞれのエンティティのテストでは変換以外のデータ要素だけにします。
このテストって、変換と逆変換がともに正しいことを期待していると思うので
すが、変換も逆変換も間違っていてたまたま assertEquals を通ってしまう場
合に対処できないと思うのですけど。何らかの形で変換が正しいことを確かめ
る必要があると思います。
> あと、Preprocessor, Postprocessorについては、必要があれば、
>
> // Input ⇒ (Intermediary ⇒) Output
> Input input;
> Intermediary inter = Input.toIntermediary(preprocessor);
> Output output = Inter.toOutput(postprocessor)
>
> こんな感じになるのでしょうか。
> # でもこうしてしまうと、変更時のテストの影響は大きそうですね。
なるほど。実際には preprocessor や postprocessor は変換アルゴリズムの
バリエーションを表す Strategy である場合がよくあるので、このコードをそ
のように受け止めることが出来そうですね。
> あんまり、私自身、単体テストが上手くいってると感じたことはない(^^;;
> のですが、もう少し良いアイディアがあれば教えて下さい m(_._)m
自分でも考えてみました。
問題は「一連の処理を分割した際に、その中のある処理の責務を変更すると、
その影響が非常に大きい(特にテストケースの変更が問題)」ということです。
責務の変更が生じる理由を考えてみますと、分割された処理も実は内部的には
いくつかの処理に分割することが出来て、その内部処理が境界を越えて移動す
る時に責務の変更が起こるのではないか、と考えてみました。
図に書くと次のようなイメージです。
変更前
+--------------------+ +--------------------+
|Process1 | -> |Process2 | -> ...
|Sub1-1 Sub1-2 Sub1-3| |Sub2-1 Sub2-2 Sub2-3|
+--------------------+ +--------------------+
変更後
+--------------+ +---------------------------+
|Process1 | -> |Process2 | -> ...
|Sub1-1 Sub1-2 | |Sub1-3 Sub2-1 Sub2-2 Sub2-3|
+--------------+ +---------------------------+
そこで、解決策としては処理を分割する時に、将来の変更の余地がないほど粒
度を細かく分割し、粗粒度の機能クラスを細粒度の機能クラスのコンポジショ
ンとして構成すればよいのではないかと考えてみました。
図に書くと次のようなイメージです。
+------------------------+
|Process1 | -> ...
+------------------------+
| | |
+------+ +------+ +------+
|Sub1-1| |Sub1-2| |Sub1-3|
+------+ +------+ +------+
テストケースは細粒度の機能クラスに対して書きます。テストケースクラス自
体は増えますが、一つ一つの機能クラスは単純なので、一つのクラスに対して
考えるべきテストケースは少なく抑えることができ、全体のテストの数はあま
り増加しないでしょう。このテストは処理の組み換えが起こっても再利用する
ことができます。
粗粒度の機能クラスに対するテストは結合テストとなるので、処理の組み換え
が起こると書き直す必要があるでしょう。しかし、細粒度の機能クラスの単体
テストが十分整備されていれば、結合テストの数を抑えることができるので、
書き直しのコストは減るものと思われます。
山崎 進 --- yamazaki@....jp
東京工業大学数理計算科学専攻柴山研