MVCでいい感じに頑張りたい!MVC + Service + Shared Service
2023/05/27
IT/IoTこんにちは、さきです!
今回はアーキテクチャのお話です。
MVCとは?というところから始めるとかなり記事ボリュームが増えてしまうので、そこは今回割愛します。
なお、この記事でいうところのMVCとはJavaなどのそれではなく、Web系のそれです(同じ概念なのですが若干印象が違うので、念のため)。
LaravelなどのWeb系フレームワークでよく見るアレですね!
この記事を要約すると
MVC + SにしてもまだServiceがFatな神クラスになることが多いので、さらにShared Serviceレイヤーを追加した5層構成にして、MVC + SSっていうのはどうですか?という内容です。
MVCアーキテクチャの抱える問題点
MVCはシステムが複雑になるにつれて「Model」「Controller」のどちらか、あるいは両方がどんどんFat(コード量が増える)になっていき、非常にメンテしにくい状態になりやすい宿命があります。
というのも、3層構成のうちV、つまり「View」レイヤーはUIのためのレイヤーですから、処理系は実質的にModelとControllerの2層で頑張らないといけないので、これは致し方ないとも言えます。
そこでServiceレイヤーを追加したMVC + S
MVCの派生系なのかMVCに含まれるのかの細かい定義はわからないですが、少なくともMVCにServiceレイヤーを追加して4層構成にしたアーキテクチャはよく見かけるパターンです。
MVC + Sの良いところは、ModelとContoller以外に処理系を書けるレイヤーを追加したことで「これどこに書いたらいいんだろう、とりあえずControllerに書いておくか」みたいなことを起こらなくし、ModelとControllerがFatになりにくくなるという点です。
ところが、これで解決よかったね、とは残念ながらならないのです。
MVC + SはServiceがFatになりやすい
Modelに書くべきことはModelへ、Controllerへ書くべきことはControllerへ、その他をServiceへ、みたいな実装になりがちなので、MVC + SはServiceがFatになりやすいです。
ModelとControllerはそれぞれの純粋な責務に専念できる状態になるのでこれでも良いと言えば良いのですが、やっぱりFatになるのは避けたいところ。テストも辛いし...。
Serviceを細かい粒度にするのはダメなの?
Serviceを細かい粒度にして、Controllerから複数のサービスを呼ぶ、みたいな感じにすればFatにならないのでは?という考え方です。
これはちょっとよくなくて、これをやる場合、DBトランザクションの管理をどこに実装するのかという問題が出てきます。
というのも、責務だとか実装的にキリのいい単位というのは必ずしもトランザクションひとつ分の単位とイコールにはならず、「細かい粒度で」という意識でクラスを分けていくと、トランザクションひとつに対して複数のServiceクラスが使用される状態になりやすいです。
そうなると、Serviceクラスを呼び出すレイヤーでトランザクションの管理をしなくてはならず、それは必然的にControllerになってしまいます。
結果、rollbackといった例外処理などの「これControllerの責務かな?」というものがControllerに実装されてしまうので良くない。
トランザクションの管理はServiceに実装するべきだと考えます。
Serviceから他のService呼んだらダメなの?
これもトランザクションの関係で厳しいです。
ロジック的な処理だけでなく、トランザクション管理の処理まで丸々呼んでしまうことになるためです。
beginした後にまたbeginする、commitした後にcommitする、みたいなことが起こります。
ただし、トランザクションの管理をしていないServiceであれば、この問題は起こらないので呼んでOK。
でも「どのServiceがトランザクション管理をしていて」「どれがしていなくて」というのは実装を見に行かないとわかりませんので、この使い分けは現実的ではありません。
さらにレイヤーを追加する
「性質の異なる2種類の概念」を同じ「Service」として扱おうとするからダメなのであって、これを別レイヤーとして分離してしまおうという考え方です。
そこで、次のような例がないか、まず探しました。
- MVCアーキテクチャ、もしくはその派生系である
- Serviceレイヤーが存在する
- Serviceをさらに細かくした粒度のレイヤー、もしくはレイヤーに近い概念が存在する
すると、近いものが見つかりました。
Spring MVCにおける「Shared Service」
「Spring MVC」というものが「Spring Framework」というJavaのフレームワークの中にあり、その中で「Shared Service」というものが使用されています。
(Javaの世界ではこれが標準なのか、それともこのフレームワーク独自の概念なのかは詳しくないのでわからず...)
Spring MVCにおけるShared Serviceとは「Serviceの処理から共通処理を切り出し、再利用可能にしたもの」というようなイメージです。命名もそんな感じですね。
Web系のMVC + S用に、概念をちょっと拡張してみる
単に「共通処理をまとめました」だけだと先述の問題に対応できないので、Web系のMVC + Sの問題を解決できるよう、Shared Serviceというレイヤーの概念を拡張してみます。
そもそもJavaなどのMVCとWeb系のMVCって同じ命名でも中身の感じが違ってたりするので、多少意味合いが変わってもそこはご愛嬌、ということで。
- 再利用の有無は問わない。どう見ても何か特定のService専用になりそうなShared ServiceだとしてもOKとする
- Serviceレイヤーからのみ使用して良い。Controllerその他から直呼びはダメ
- Serviceには依存してはいけない。Serviceで何の処理をしているのかというところは意識せず、ServiceがShared Serviceに依存するものとする
- トランザクションを管理してはいけない(というよりも、トランザクションを意識してはいけない)
- 1つのShared Serviceは1つの責務を持つ
- つまり、基本的にはServiceから複数のShared Serviceが呼ばれる
こういう感じであれば、MVC + Sの問題点をカバーできそうです。
一応Serviceクラスからは制限なく呼べる存在ですから「Shared」という命名も(多分)矛盾はしてないと...思います。
イメージ的には次の画像のような感じ。
ちなみに、(ドメイン駆動設計:DDD風にいうところの)ドメインロジックがどう、アプリケーションロジックがどう、という切り分けはここでは考えません。
"その評価軸"でレイヤーを区切っていくと、それはもうMVCの原型を留めなくなるからです(主にModel)。
あくまでMVCの発展系?のMVC + Sをさらに発展させたようなものを目指しています。
この方式のメリット
クラスの責務がはっきりし、また、クラスあたりの実装量が減るのでコードの見通しが良くなりスッキリします。
これはコードレビューする際にも良い効果があると考えています。
また、テストが格段にしやすくなります。
Serviceで一塊になっている処理をテストするとなるとかなり複雑になりますが、Shared Serviceを使用することでMockを使いやすくなり、テストも書きやすくなります。
まとめ
Web系のフレームワークの有名どころはMVCアーキテクチャが多く、新規に立ち上げるシステムでもない限り「MVC前提」で既存実装が書かれていることが多いです。
そこにMVCとは毛色の異なるアーキテクチャを部分的に導入しようとしてもうまくいかないことが多く、それであれば「MVCという大きな流れに身を任せたまま、良い感じの実装はできないだろうか?」と考えたことがきっかけでした。
実際にもうこのMVC + SS(仮)を使ってみましたが、今のところかなり良い感じです。
モダンなアーキテクチャももちろん良いものですが「導入できないなら思考停止のMVC」みたいにならず、MVCはMVCで良い感じにできるのではないか、という内容でした!