Let's read!
さあ、いきなり、コードを見てみてください。
public void index() { // what do you select? ... = logic.selectNandemoMember(null, "S", null , false, true, false, CDef.MemberStatus.Formalized , true, false, true, true); }
Action, Controller, Service相当のクラスが、そのLogicのメソッドを呼んでいると思ってください。
こういうメソッド見たことありませんか?読みやすいですか?
さて、Logic側はこうなっています。(省略してますけど、もっとデカくなります)
public List<Member> selectNandemoMember(Integer memberId , String memberName , Date birthdateFrom , boolean hasFormalizedDate , boolean existsPurchase , boolean existsLogin , CDef.MemberStatus statusCode , boolean setupSelectService , boolean loadPurchase , boolean loadLogin , boolean exceptMobileLogin) { List<Member> memberList = memberBhv.selectList(cb -> { if (memberId != null) { cb.query().setMemberId_Equal(memberId); } ...(more) if (hasFormalizedDate) { cb.query().setFormalizedDatetime_IsNotNull(); } if (setupSelectService) { cb.setupSelect_MemberServiceAsOne(); } ...(more) }); if (loadPurchase) { memberBhv.loadPurchase(memberList, purCB -> { ...(more) }); } ...(more) return memberList; }
あなたが目撃者だったら
さて、あなたはいま新しい画面の実装しています。
このメソッドを呼びたいと思いますか?
...
いざ呼ぼうと思ったら、
ちょっとだけ絞り込み条件が足りませんでした。
さて、どうしますか?
読みづらさ大爆発
jfluteは何度か見たことがあります。それはもう "メンテ不能の強者" で、どんな検索をしているのか?それを確認するのに、ソースをあっちいったりこっちいったり。
まだ、引数が固定値であれば、実際に実行してログのSQLを見ればという感じですが、引数が RequestParameter によるもので、引数の指定があったりなかったり、引数を指定する側でも分岐があったりすると、もう無理です。
ちなみに勘違いしてはいけないのは、厳密には引数が多いからつらいのではなく、「引数でメソッドの処理を細かく操作しようとしている」という点です。それで引数が多いとなおさらと。(単にデータベースに登録する値とかであれば、引数クラスにまとめようくらいの話で済みます)
デグレの温床
Javaでは引数名指定のメソッド呼び出しができませんが、仮にできたとしても、50歩100歩です。
読みづらさだけではなく、これだけ選択肢があるということは、だいぶ業務の違う画面同士で再利用されています。Logic内では複雑な分岐があり、修正には勇気が要ります。
大抵、その勇気はありませんから、追加的な修正だけを行っていくと思います。理想的な修正をして綺麗に整えていくならまだしも、どんどんパッチ的な追加修正ばかり積み上げていくので、大抵、引数デザインが酷いことになりがちです。(そう、引数デザインが大事なパターンですが、引数デザインって難しいものです)
引数のネーミングや、その抽象度もポイントです。引数から何をやっているのか想像が付きやすいかどうか?パッチ的な積み重ねをしていくと、なかなか、そういう引数にはならないものです。
なので、名付けました。
あたかも、引数というリモコンで、遠くの ConditionBean を(不器用に)操作する姿、そこから...
引数リモコンパターン
と名付けました。
もう、これは、ConditionBeanの良さを打ち消し、独自のインターフェースで検索を提供する、"独自のO/Rマッパー" と言えるでしょう。
いったい誰が作るの?
多くの場合、特定の誰か一人が作るわけではありません。たぶん、最初に作った人も、引数リモコンパターンが好きなわけではないでしょう。
恐らく最初は引数は三つくらいで、まあこの程度なら...と思ってサクッとやったくらいな。
jfluteも、ちょっと引数にboolean渡して分岐とか、別にやらなくもないです。ただ、できるだけ内部的なものに限定したり、絶対に数が増えない状況であることを確認したり、できるだけ違う方法での表現を模索したり、特にこのパターンを名付けてからは気をつけてます笑。
そう、最初はそんなに引数は多くない。それくらいだったらまあ別にそんなつらくもない。でも、大抵チーム開発ですよね。別の人がそのメソッドを再利用するとします。でも、ほぼ使えそうなんだけど、ちょこっとだけ違う。
そのとき、別に再利用せずに自分で0から実装してもいいのに、再利用目的意識の強いLogicクラスにそういうものがあると...
「なんか、これを使わなきゃいけないのかな...」
って思い始めます。
確かに自分の要件と似てるから、使えるならその方が楽だし。
ということで、ちょこっと自分用の引数を追加して、自分の要件を満たします。そして...
while (true) {
また別の人が...
}
というか、元々の作った人も、画面の要件が変わって、また追加修正したり...「あれぇ、なんか引数随分増えてるなぁ、修正しづらいなぁ...」
誰もこれだけ巨大な引数リモコンパターンを作ろうとは思っていなかったのに、見事にできあがってしまいました。インクリメンタルな開発をする事業会社の方が発生しやすいかもですね。
そう、引数リモコンパターンはどんどん育てられていくのです。やめようと思ったときには時すでに遅し。
「本当にこういう検索でいいのか?」
を、確認すること自体が大変なので、一個一個、呼び出し側の要件を紐解いていっての修正、非常にコストがかかります。
ルーズな再利用ならしない方がいい
ずばりこれです。
へたな再利用ならしない方がマシ。
jfluteは、"検索まるごとの再利用" って、できることはあまりないだろうと思っています。少なくとも BtoC などサービス系だと、
同じテーブルの検索にしたって、結局は画面によって、データ取得したいものが変わったり、並び順が変わったりするものです。
ConditionBean的に言うと、SetupSelect や LoadReferrer, OrderBy は、画面によって微妙に変わってくると。
A画面では関連テーブルのデータが20個必要、B画面では関連テーブルのデータが3個だけ必要、それ以外はだいたい検索条件は一緒。じゃあ、B画面でもA画面用の検索メソッドを呼んで...B画面を開くと17個の無駄な関連テーブルのデータが...
という、最小公倍数パターンも酷いですが、それを避けるために、引数に boolean をどんどん追加していって、引数リモコンパターン化していくのもつらいです。
そこまで無理して再利用しなくていいのでは?
というか、厳密な再利用にもなっていないのではないか?とも考えられます。
再利用は、"何か変わったときに一箇所直せばOK" っていうの嬉しい。このケースだと、呼び出し側のそのメソッドに対する "業務的な意味" がそれぞれ統一されているとは限らないので、何か条件を変えてもすんなりOKとはなかなかいかないでしょう。(それを確認しなかったとすれば見事なデグレです)
再利用は、"実装するときが楽" っていうのが嬉しい。もはや読みづらく挙動を厳密に確認しないと使えないメソッド、再利用する方が実装コストがかかってしまうかも。ConditionBeanを使っているならなおさら、ササッと書いてしまった方が速いでしょう。
これは、「何を再利用しているのか?」が、ハッキリしていないことが原因です。再利用しているものは確かにあるだろうけど、それがノイズ (分岐ゴミ) に紛れていて、はっきりしないから再利用の恩恵を受けられない。へたな再利用の典型だと考えます。
...
再利用しまくって、メンテするの怖いから、パフォーマンスチューニングもできない
とか、そんなの最悪ですね。
もっと、再利用したいものは他にもあるはず。再利用体力には限界がありますから、節約して、本当に再利用したいものに使って欲しい。
where句の部品再利用
でも、検索の中で再利用したいものもあります。それは絞り込み条件の一部分です。つまり、where句。
例えば、"特別過ぎる優待会員" という業務概念があったとして、それを表現する絞り込み条件が以下のようなものだとします。
cb.query().setMemberName_LikeSearch("S", op -> op.likePrefix());
cb.query().setMemberStatusCode_Equal_Formalized();
cb.query().existsPurchase(purchaseCB -> {
purchaseCB.query().setProductId_Equal(SPECIAL_PRODUCT_ID);
});
様々な画面の検索で、この三つの条件をベタベタ書くと、単に書くのが面倒だし間違える人もいるって話に加え、その業務概念の条件が変わったときに窮地に陥ります。
"特別過ぎる優待会員" という絞り込み条件の具体的な実現方法は、一箇所にしたいものです。(サービス系のインクリメンタル開発ならなおさら)
それならば、そこだけを再利用するような部品メソッドを作ってみんなで共有するようにしたいところです。
例えば、DBFluteの自動生成クラスは、ジェネレーションギャップパターンで生成されていますので、Exクラス (一度自動生成されたら上書きされないクラス) の方に、独自に "特別過ぎる優待会員" を表現するメソッドを定義。
public class MemberCQ extends BsMemberCQ { ... /** * 特別過ぎる優待会員 */ public void arrangeServiceMember() { setMemberName_LikeSearch("S", op -> op.likePrefix()); setMemberStatusCode_Equal_Formalized(); existsPurchaseList(purCB -> { purCB.query().setProductId_Equal(SPECIAL_PRODUCT_ID); }); } }
これを、それぞれの検索で再利用します。SetupSelect したいものが違ったり、OrderBy したいものが違っていたり、他の絞り込み条件があったりしても関係ありません。
memberBhv.selectList(cb -> { cb.setupSelect_... cb.query().arrangeServiceMember(); // *here cb.query().set... cb.query().addOrderBy_... }
基点テーブルが変わっても構いません。
purchaseBhv.selectList(cb -> { cb.setupSelect_... cb.query().queryMember().arrangeServiceMember(); // *here cb.query().set... cb.query().addOrderBy_... }
部品再利用の積み重ね
少なくとも DBFlute では、検索まるごとの再利用よりも、部品再利用の方をオススメしています。
検索まるごとの再利用をするならば、引数リモコンパターンにならないように、うまく設計する必要があるでしょう。そのためには、専門のマネジメントが必要でしょう。それなりに費用がかかるものだと思います。
なので...
検索まるごとの再利用ができるパターンは、なくはないけれども、一つのシステムにおいて、そんなに数は多くないと想定しているので、そのくらいの再利用は目をつぶってもいいかなと。
それよりも、部品単位で小さい再利用を積み上げる方が、再利用の量は減るかもしれませんが、実際の現場にはフィットすると考えています。また、DBFlute (ConditionBean) は、それがやりやすいのが特徴だと思っています。
DBFluteのオフィシャルサイトにも、この話が書かれています。
=> where句の再利用 | DBFlute
ルーズなDaoパターンだと発生しやすい!?
これは、また別途ブログ書きますね。
=> 【追記】ルーズなDaoパターンなら見たくない
この引数リモコンパターンが発生した現場では、ほとんどにおいてDaoパターン、もしくは、それに等しいレイヤで検索をしているパターンでアーキテクチャデザインされていました。
再利用される想定で作ってない検索メソッドも、再利用を意識したレイヤに定義してあるので、他の人がどんどん育てていってしまい...