アーキテクチャ設計における悩みなど
自分がプログラミングを学習しはじめたのは10年以上前なんですが、当時はいわゆるMVC(Model - Controller - View)でアプリケーションを構成することが一般的でした。
簡便でわかりやすい設計ではありますが、ありがちなパターンとしては
- ビジネスロジックがControllerに集中し、FatController状態になる
- FatControllerゆえにテスタビリティが低い
- Modelにテーブル定義とビジネスロジックが混在している
などなどさまざまな問題が起こりました。
当時はとにかくまず動かすのがゴールだったので、コードをきれいに書くなんていう概念もあまりなかったと思います。
それから長い年月がたち、いろいろな現場でいろいろなソースを書いてきました。
んで、やればやるほど悩むことは増えます(爆)
ただ、少し前の設計にこだわる現場で仕事をし、汎用的で使えそうなアーキテクト設計の答えがでたので、感じたことなどをメモしておこうかと思います。
もちろんこれが最適解なんて言うつもりはなく、クリーンアーキテクチャーを考えている方の参考になればと思います。
ちなみに、PHP(Laravel)をメインに説明していますが、あくまでも題材の一つであり、言語はなんでもいいと思います。
ソースの構成など
├─┬ Console/
│ ├─┬ Commands/
│ └── Kernel.php
├─┬ Exceptions/
│ └── Handler.php
├─┬ Http/
│ ├─┬ Controllers/
│ ├── Kernel.php
│ └── Middleware/
│ └── Requests/
│─┬ UseCases/
│ ├─┬ User/
│ ├ StoreAction.php
├─┬ Services/
├─┬ Infra/
├─┬ ValueObject/
├─┬ Models/
├─┬ ViewModels/
└── Providers/
いろいろと試行錯誤したのですが、現段階で上記のようなソース構成がベターなのかなと思っております。
以下各フォルダの役割について。(自明なものは避けて、説明が入りそうなものに関して。)
ポイントとしては 「各階層の責務を分離させる」 ですかね。
Controllers
以前はビジネスロジックをここにゴリゴリとかいていましたが、あくまでリクエストを受け付けて、レスポンスを渡すだけ。
業務ロジックに関するロジックはここに書かないのがポイントだと思います。
Requests
リクエストパラメーターのクラス。
Controller内部で$request->all()
なんて書いてましたが、そもそもControllerで値を受け取る前に引数の段階でRequestという形で値を定義し、バリーデーション自体もここで対処してしまうほうがきれいではあります。
(私は以前、Formディレクトリなんてのを作ってバリデーションはここに書いてましたが・・・)
似た概念としてAPIであればレスポンスを返すResourceクラスのようなものを作ってみても良いかもしれないです。
Service
メインの業務ロジックが入る箇所です。おそらく一番判断に迷う箇所かと思います。
ポイントとしては以下のような点でしょうか。キーワードはやはり責務の分離になります。
- 再利用できる構成を意識すること。疎結合にすること
- 直接SQLを書くなどモデル層に分類される処理をなるべく書かないこと
- S3や外部ストレージに関わる処理を書かないこと
これ以外にトランザクションスコープをどこで定義するのか、というのも議論が分かれる箇所かと思います。
Controllerで書いてしまうと業務ロジックを入れてしまうことになり、Service層に書いてしまうと再利用ができなくなってしまいます。
当時の現場ではAction層をControllerとServiceの中に入れてトランザクションをここで貼っていましたね・・・
Infra
S3やRedisなど外部サービスとの連携に関する処理はService層の中に書かず別途ここで分離した方が良いと思われます。DBなどのレイヤーに関わる処理をService層にかかないのと同じ理由です。
csvの吐き出しみたいな処理はいろんな画面で共通になることが多かったですね。
引数をcallbackで受け付けてこの中で吐き出す・・・みたいな処理が中心でした。
ValueObject
業務で重要になってくる伝票情報などの文字列などの情報はここに。
理想論を言うと電話番号やメールアドレスも全てここにまとめるようですが、そこまではコスト的にどうかな、と思います。
アプリケーションの規模や予算との相談になるでしょう。
会員番号などの業務上特定の意味を持つ文字列や番号に制約を付けて管理することで、値の正常性を担保し、不整合な値を排除することができます。
値オブジェクト(Value Object)
値オブジェクトがでてきたので、ここにメモしておこうと思います。
これは業務上で使われる値を数字や文字列などといったプリミティブ型ではなく、制約を持たせたクラスとして定義することです。
例としては会員番号などの業務上特定の意味を持つ文字列や番号などに付けるのが最適です。(例えば先頭文字Mで始まり、それ以降は半角数字7桁など。)
メリットとして以下のようなことが挙げられると思います。
- 型やサイズなど値の判定をインスタンス生成時に行うことができる
- 値の正常性を担保でき、不整合な値などはインスタンス生成時に排除することができる
- 生の値に直でアクセスせず、制約を付けることでイレギュラーな挙動を制御できる
- 業務での関心事とソースの構造の粒度を合わせることができる
- セットした値は基本変更しないことで制約を強くすることができる
- 少し近い概念として可変的な値ではないですが、ENUMなど
ファーストコレクション(コレクションオブジェクト)
値オブジェクトと近い概念で配列やリストに対して、値と関連する処理を1つのクラス内でまとめて表現したものをファーストコレクションというようです。
例として、ECの注文商品など。
- データに対する操作に対しての制約を付けることができる
- データに対する操作の責務を一任できる
- コレクションを不変にして外部に渡すことであるクラスの挙動を一任できる
- 外からアクセスできないようにすることで値の有効性を担保できる
以前はただの文字列や配列で定義していますが、これらの概念を取り込むことによって、責務の分離をきっちり行うことができ、値自体の担保や可読性も向上します。
Model
Entityなんて呼び方をする現場もありますね。
要はテーブルと1対1に近い関係です。
SQLに相当するパーツを描く場所は厳密にはRepository層になりますね。
私がいた現場はEntityとRepositoryを分けずにあつかっていましたが、現場によっては用途によって分けてもいいのではと思います。
ViewModel
viewに値を渡すとき、テンプレートの部分にゴリゴリと条件分岐、ループなどを書くことがあったのですがNGです。
特に画面を読み込んだ時と、バリデーションなどで戻ってきた場合は変数の読み込み箇所などが大きく変わるので条件分岐が複雑になるケースが多いのですが、
最適な書き方としてはviewModelというオブジェクトでまとめ、この中で条件分岐の表示処理などを記載するのが良いでしょう。
伝票や商品など情報が多く、分岐などが多くなりがちなケースで必須かとおもいます。
UseCase
議論が分かれるところかもしれません。定義をあえて決めておくと、「利用場面ごとに、どのような処理が行われるのかを明確に定義したもの」
当時の現場ではControllerを受け取り、Serviceをまとめる層を作っております。
特徴としては、
- Controllerと1:1になる
- 再利用性のあるビジネスロジックはなるべくかかない
- Transcationをここに貼る
などになるかと思われます。
これがあることでUseCaseをMock化することで、ControllerのInとOutのテスト(特にリクエストパラメーターのテスト)がかきやすくなります。
命名に関して
最後はやはり命名が迷いどころですよね・・・
これもこだわれるポイントがいろいろあるなあと思っております。
以下のような点に特に注意しておりました。
- なるべくシンプルかつ統一性のある命名にする(一覧=index、新規作成=add、詳細=show・・など)
- オブジェクトに関しては基本名詞で動詞を入れない
- リソース間での品詞を統一する
- 業務でのリソースの扱われ方に注意する
- 汎用的になりすぎない
- メソッドに関して重複をしない(CustomerServiceでgetCustomerDataなど)
- クラス名が複雑になりそうな場合は、2階層以上に分ける
大事なポイント
いろいろ書いてきたのですが、大事なポイントとしては
- 責務の分離をすることで各層の責任が明確になり、どこに何をかけばいいのかが明瞭になる
- それにともないソースの可読性が大きく向上する
- 上記のように細かくモジュール化することで各層のテストが容易になり、品質の向上につながる
- 最後は当たり前ですが、現場の現状に合わせる
などでしょうか・・・