Logicの実装デザイン
LastaFluteからの提案です。
このページは、Actionの実装デザインを読んでいることが前提です。
いきなりまとめ
実装デザインの提案
本気でDDDをやるフェーズまで達していない段階、かつ、プロジェクトで独自にアーキテクチャを定義していない場合の、LastaFlute からのデフォルト実装デザインの提案です。
- Action (バウンダリ、かつ、ファサード: Transactionあり)
- フローコントロール、そして、再利用しない検索や更新など(ちょこっとロジック)を書く
- WEB依存 "する" 処理の再利用や、ちょこっと役割分担は、(Action)Assist
- WEB依存 "しない" 処理の再利用は、(Business)Logic
- Logic (ビジネスロジック)
- WEB依存しない再利用するものを書く (二箇所以上で呼ばれて初めて Logic に)
- ただし、検索の再利用はできるだけ ArrangeQuery にて (Logicでのまるごと再利用は限定的に)
- Logicの名前は、"モノ+Logic" はダメ、できるだけ "モノ+業務+Logic"
Action は、こちら
Actionは、また別途まとめています。
というか、Actionの方から先に読んだほうが良い です。Actionの持つ役割の前提があってこそ、ここでの Logic になっています。
Logicに何を実装するか?
Logicは、非常に粒度を設計するのが難しいクラスです。画面単位?テーブル単位?いやいや業務単位? しっかりとDDDをやらない限り、業務単位というのもなかなか適切なデザインをするのは難しいです。
再利用するものだけをロジックに!
ここはなかなか答えを出すのが難しいところですが、LastaFluteでのひとつの提案があります。
☆☆☆ 再利用するものだけロジックに書く ☆☆☆
再利用し過ぎるよりかは、少し足りないくらいの方がスタートアップには向いていると考えます。
ルーズなロジックだと?
曖昧なまま、Logic運用をするプロジェクトがたくさんあります。 そこで発生する問題は大抵同じです。
- 潜在的に画面に依存したロジック
- 汎用的に見えて、実際には (潜在的に) 固有の画面に依存したロジック。再利用すべきでないロジック。
- これ自体が悪いというより、これが Logic に入っていると、ついつい別の画面が使ってしまうことがあります。 そうすると、元々の画面のロジック変更をしたとき、別の画面に不具合が発生する可能性があります。 ちゃんと修正するときに確認はすべきことですが、ロジックの扱いが曖昧だと、修正する方は他の画面が使ってると全く思わず作業してしまいます。
- そうなってくると、本来再利用すべきロジックも "なんか怖くて呼べない", "呼んでいいのやらいけないのやら..." という気持ちになってきて、本来ロジックが持っている大きな役割の一つである "再利用" に支障が出てくるのです。
- ソースを読むのにあっちこっち飛ぶ
- そもそも再利用していたり、業務上の意味が明確で、ネーミングもわかりやすいものであれば、それは問題ないですが、 なかなかそうはならないです。たくさん command + click でソースを飛んで飛んで読んでいても、実際には全部その画面固有の処理だったなんてことも...
- 綺麗に役割を分けて書くというのは、想像以上に難しいことです。プログラムが書けるからそれができる、とは全く言えないと考えます。 ヘタに分けるくらいだったら、分けない方がまだマシというようなことも多くあります。
二個目が見えてないときは難しい
将来の再利用を目的に Logic に切り出すとします。厳密にはそれはとてもよいことです。 しかしながら、そのときはまだ再利用するもう一個のリクエストが見えてない状態です。 その状態で二個目のリクエストに本当にぴったりな再利用メソッドに最初から仕上げるのはなかなか高度なことです。
よくあるケースで、"再利用できそうなんだけど、ちょっとだけ違うんだよね" と、実質最初のプロセス固有の処理が入り込んでしまったりして、結局再利用できないことも。 すると、ここでなかなかつらいことが発生します。
- コピペパターン
- コピーして、少しだけ直したものを新しく作ってしまう
- リモコンパターン
- 一つ目のメソッドの引数にbooleanを追加して分岐させる
放っておくと大抵はコピペパターンになります。
リモコンパターンが積み重なると、悲劇を生みます。
単純に処理の違いだけでなく、引数や戻り値などのメソッドデザインが、二個目のリクエストに合うとも限りません。 二個目の人にとっては使いづらくて、結局使わないで自分で書いたり、先ほどのパターンにハマったりすることも多いです。
再利用するときに切り出せばいい
インクリメンタル開発は、運用中の既存のコードを修正するのが当たり前なので、ある意味で既存のコードを修正しやすい環境とも言えます。 なので、適切な再利用が見えてきてから再利用をするように切り出すことは比較的しやすいのです。
もちろん、再利用メソッドの経験値の高い人はよいですが、そうでないならば、将来を見越した再利用メソッドは無理をする必要はないと考えます。 なので、ひとまずは Action クラスに書いてもいいでしょうと。それが恒久デザインなわけではないので。
もし、自信持って再利用メソッドをあらかじめ作るのであれば、JavaDocなどはしっかり整備して、再利用する人が迷わないようにしましょう。
No more, アバウトLogic!?
よく巨大な Logic を見かけませんか?
e.g. No way, logic should not extend action @Java
public class SeaLogic { // ★名前がアバウト
// ここにメソッド100個くらい...ってちょっとおおげさだけど、ちょーたくさん
}
大抵、MemberLogic, ProductLogic というアバウトな名前のクラスであることが多いです。 Memberに関するLogicだから、Memberに関するものがなんでも入ります。
こういったクラスがすでに存在していると、後から実装する人は、当然 Member に関するものは MemberLogic に入れます。"ちょっとでかいなぁ、やだなぁ" と思っても、そのまま MemberLogic に入れることがほとんどでしょう。MemberLogic がすでにあるのに、MemberSignupLogic は作りづらいですから。 (すでにあるクラスに入れる方が楽と思うでしょうし...MemberLogicに入れなきゃいけないんだと思うでしょう)
Logicはどういう単位で作っていくべきでしょうか?
これは難題です。恐らくちゃんとやるなら、DDD的なアプローチが必要になるでしょう。 でも、スタートアップではなかなか最初から徹底するのは難しいものです。
でも、Logicを作って再利用はしたい...
少なくとも、モノ + Logic だけのクラスは作らないこと。そう、MemberLogic です。
小さな提案、最低限 モノ + 業務 + Logic にすること。 MemberSignupLogic です。もし、業務自体で暗黙にモノが特定できるなら、業務 + Logic だけでも良いでしょう。SignupLogic ですね。でも、モノ+Logicにはしない。
e.g. object + business + logic @Java
public class SeaInParkLogic { // SeaにInParkするLogic
// したら、さすがに In する以外のメソッドが追加されることは...あまりないはず
}
PurchaseLogic は迷いますね。Purchaseはモノなのか業務なのか... でも、購入というイベントにも色々なものがあるかと思います。 購入フロー自体に加えて、購入商品の支払い、購入履歴の確認、購入のキャンセルなどなど、購入というデータに対して、様々な振舞いが存在します。 安易に PurchaseLogic を作ると、それらがすべて後からどんどん追加されていきます。
その結果、一つしかメソッドのないLogicができるかもしれませんが、別にそれは構わないかと思います。 インクリメンタル開発では、後で追加される可能性は大ですから。
モノは名詞とは限らないし、業務が動詞とは限りません。 そこが難しいのかもしれませんが、少なくとも Logic を作るときにちょっと考えてみると、良い名前が見つかるかもしれません。
No more, Logic が Action 継承!?
よく新人プログラマーの実装で見かけることがあるかと思います。
e.g. No way, logic should not extend action @Java
public class SeaInParkLogic extends DocksideBaseAction { // ★絶対ダメ
...
}
Logic は Action ではありません。つまり、"is a" の関係になっていません。
では、なぜ "is a" の関係になっていないといけないのでしょうか?
Actionのスーパークラスは、Actionに最適化しています。Actionを形づけるために存在しています。 Logicのために存在しているわけではない のです。
ゆえに...
(Actionを継承している)Logicに都合の悪い修正が平気で入ります。
もし、Actionのスーパークラスの修正で、それを継承している Logic が落ちたとしたら、Action を継承している Logic の方が良くないと言えます。
もともとオブジェクト指向の目的として、意味 を重視して、概念ごとにクラスを取り扱うことでプログラムを整備しやすいという点があります。 "XxxLogic は Action である" と自然言語にして しっくりくるかどうか? ここがポイントです。
便利なメソッドが欲しければ、Logicがその便利なクラスに依存すればよいと考えます。 それが、Actionのスーパークラスにベタッと書いてあったのであれば、別途クラスに抽出して再利用して使うようにしましょう。
一方で、Logicのスーパークラスを作っても悪いわけではありませんが、スーパークラスのメインの役割は その概念を形づける なので、特に Logic を共通的に形づけるものはあまりないかなと考えるので(Actionが非常に定型的で特殊と言えます)、大抵の場合は全体共通のスーパークラスはあまり必要ではないと考えています。 もちろん、まとまった業務単位で必要であれば作っても良いでしょう。
No more, Logic が WEB に依存!?
もっと、"それ以前" 的な話ですが... 時々見かけるコードにこういうのがあります。
e.g. No way, logic should not depend on web @Java
public class SeaInParkLogic {
public void land(SeaForm form) { // ★絶対ダメ
}
}
先の通り、Logicは再利用を目的 にしているため、とある固有の画面のFormクラスを参照すると、別の画面で再利用がしづらくなります。 FormクラスはWEBサイドに特化しているので、こちらも先に述べた潜在的にWEBに依存したロジックとなってしまいます。
仮に、B画面がA画面のFormをnewして無理矢理再利用したとしましょう。 でも、A画面のFormはA画面に最適化されていて、いかにもA画面だけで使っていそうなクラス名やパッケージに見えます。 でも実はB画面が使っている...となると、B画面やLogicのことを気にせずにA画面都合の修正が入り、B画面やLogicが落ちる というトラブルが発生しやすくなります。ひとりで作っていればそんなことはないだろうと思われるかもしれませんが、インクリメンタル開発では様々な人が入れ替わり立ち替わりコードを修正することが想定されるため、再利用すべきもの再利用すべきでないものは明確にしたいという気持ちがあります。
アーキテクチャ的にも、完全に逆参照となります。
どうしても引数が多くてまとめたい場合は、別途パラメータークラスを作成すると良いでしょう。
e.g. Parameter class for SeaInPark @Java
public class SeaInParkParam {
public Integer piariId;
public String dstoreName;
public LocalDate bonvoDate;
public LocalDateTime ambaDateTime;
public Boolean miraco;
...
}
e.g. Logic accepts Param @Java
public class SeaInParkLogic {
public void land(SeaInParkParam param) {
}
}
e.g. package of parameter class @Directory
app
|-logic
| |-sea
| | |-SeaInParkLogic.java
| | |-SeaInParkParam.java
|-web
| |-sea
| | |-SeaAction.java
| | |-SeaForm.java
概念的な話が難しければ、app.logicパッケージからapp.webパッケージを参照してしまっていないか? という視点で考えて頂ければと。
Logicは、ずばり ビジネスロジック ということで、極端な話 "バッチのプログラムから呼ばれても大丈夫" というような状態をキープするものです。少なくとも、LastaFluteではそのように想定しています。 なので、Session や Cookie にも依存してはいけません(SessionManager や CookieManager を DI してはいけません)。
e.g. No way, logic should not depend on web @Java
public class SeaInParkLogic {
@Resource
private RequestManager requestManager; // ★絶対ダメ
@Resource
private SessionManager sessionManager; // ★絶対ダメ
@Resource
private CookieManager cookieManager; // ★絶対ダメ
}
どうしても、WEBに依存した状態で再利用がしたければ、それは ActionAssist を使いましょう。