TL;TR
こんなのを作ると便利だよ、という話
public class OneOf<A, B> {
final Optional<A> a;
final Optional<B> b;
public <C> C apply(Function<A, C> AtoC, Function<B, C> BtoC) {
if (a.isPresent()) {
return AtoC.apply(a.get());
} else {
return BtoC.apply(b.get());
}
}
}
前文
ある入力から、異なる2つのクラスのインスタンスのどちらかを作りたくなるとき、ありますよね。(様々なレコードが集約されたログファイルや帳票を読み込んで、その内容ごとに別のインスタンスを作りたいときとか。)
また、それらの異なるインスタンスを最終的に集計するために単一クラスのインスタンスに変更したいときもありますよね。(インスタンスの内容をNoSQLに突っ込むために、SolrInputDocumentに変換したいときとか。)
例えばFirstObjectクラス(ログファイルの各行)から、SecondObjectAクラス(アクセスログ)とSecondObjectBクラス(購買ログ)のインスタンスを作って、FinalObjectクラス(SolrInputDocument)に変換したいとすると、こんなコードになります。
FirstObject firstObject = getFirstObject();
FinalObject finalObject;
if(firstObject.isTypeA()){
SecondObjectA secondObjectA = transformA(first);
finalObject = finalTransformA(secocndObjectA);
}else{
SecondObjectB secondObjectB = transformB(first);
finalObject = finalTransformB(secocndObjectB);
}
前提:まずクラス設計を見直そう
一番良い解決策は、SecondObjectA、SecondObjectBに共通の親クラス(かインターフェース)SecondObjectを持たせて、SecondObject FirstObject.transform() を持たせるよう、クラス設計を変更することです。
FirstObject firstObject = getFirstObject();
SecondObject secondObject = firstObject.transform();
FinalObject finalObject = finalTransform(secondObject);
が、世の中そうはいかないこともよくあります。クラス設計が複雑になってしまったり、2重継承の問題で継承できなかったり、そのクラスを使わざるを得ないことも多々あります。この記事はそういうときの次善策になりうる方法について論じています。
問題点
最初のコードに立ち返ったときの問題点を見てみましょう。
FirstObject firstObject = getFirstObject();
FinalObject finalObject;
if(firstObject.isTypeA()){
SecondObjectA secondObjectA = transformA(first);
finalObject = finalTransformA(secocndObjectA);
}else{
SecondObjectB secondObjectB = transformB(first);
finalObject = finalTransformB(secocndObjectB);
}
- 記述がもさっとしている
- transformAとtransformBが機能的には並列しているのに遠くにある
- if,elseのブロックの中が大きくなった時に、コードを頭から追いづらい
- firstObject.isTypeA()
- 判定基準が単純でない場合、isTypeAメソッドは肥大化・冗長化しがち … FirstObjectがバイナリやInputStreamなんかを扱うときに、isTypeAとtransformA,transformBのコードは重複する部分が多い
- ラムダの中には突っ込みづらいし、Streamの操作には向いてなさそう
たまに見る解決策:beanを作る
SecondObjectA, SecondObjectBが共通のインタフェースや親クラスを持てない場合の解決策として、両方のインスタンスを持ったbeanをが作成する方法を見かけることがあります。
public class MyBean {
private SecondObjectA secondObjectA;
private SecondObjectB secondObjectB;
// 以下getter/setterが並ぶ
}
こうすることで、呼び出し部分は共通親クラスを導入したのと同様に書けます。が
- 大抵こういうところからNPEが発生する(Immutableに作る、コンストラクタを隠匿して値を一意に持つようにする、などで対処可能だが)
- 変換メソッドのところに if(myBean.secondObjectA != null){ hogeuhga~ }else{} みたいなやばいコードが作られやすい
- 「ふたつのうちどちらか一方のインスタンスを持つ」組が複数組あるとき、大量のbeanクラスが作られる
と、いけてない現象を引き起こす温床になります。
解決策
こういうものを作ると
public class OneOf<A, B> {
private final Optional<A> a;
private final Optional<B> b;
private OneOf(A a, B b) {
this.a = Optional.ofNullable(a);
this.b = Optional.ofNullable(b);
}
public static <A, B> OneOf<A, B> OneOfA(A a) {
return new OneOf<A, B>(a, null);
}
public static <A, B> OneOf<A, B> OneOfB(B b) {
return new OneOf<A, B>(null, b);
}
public Optional<A> getA() {
return a;
}
public Optional<B> getB() {
return b;
}
public boolean isA() {
return a.isPresent();
}
public boolean isB() {
return !isA();
}
public <C> C apply(Function<A, C> AtoC, Function<B, C> BtoC) {
if (isA()) {
return AtoC.apply(a.get());
} else {
return BtoC.apply(b.get());
}
}
public static <A, B, C> Function<OneOf<A, B>, C> totalize(Function<A, C> AtoC, Function<B, C> BtoC) {
return (one) -> one.apply(AtoC, BtoC);
}
}
こういう感じで書けます。
FirstObject firstObject = getFirstObject();
OneOf<SecondObjectA,SecondObjectB> secondObject = transform(first);
FinalObject finalObject = secondObject.apply(this::finalTransformA,this::finalTransformB);
あるいは、最初のオブジェクトはストリームで渡されるかもしれません。そんな時は
Stream<FirstObject> firstObject = getFirstObjectStream();
Stream<FinalObject> firstObject.map(this::transform).map(OneOf.totalize(this::finalTransformA,this::finalTransformB));
こんな感じ。
結語:そんなに多用するものでもない
といった感じで、OneOfクラスを導入すると、すっきりと簡素にかける場合があります。クラス間の関係を気にせず使えるので、既存クラスにも使えるので便利なことも多々あります。
ただし「ふたつの自作クラスのうちのどちらか」が必要になったときは、だいたいクラス設計が良くない場合があるので注意しましょう。(自作クラスでも効果的に使えば、効果的です。ボキャ貧)