こんにちは、請求管理ロボ開発チーム所属の塚本です。
今回は、請求管理ロボの一部をリファクタリングし、クリーンアーキテクチャ化にした話について書いていこうと思います。
請求管理ロボでは、Moneytree LINK API(Moneytree社)というサービスを使用して、請求管理ロボの請求元銀行口座にMoneytreeの口座情報を連携する「Moneytree連携」という機能を提供しています。
今回はこの「Moneytree連携」機能に関するモジュール群をリファクタリングし、クリーンアーキテクチャ化することを試みました。
なぜ「Moneytree連携」機能でクリーンアーキテクチャ化を行ったのか
1. 設計面の課題
- 設計面で当初に考慮していなかった課題が散見され、保守性が悪化している。
- モジュールの責務が肥大化しており、DBアクセスからAPIリクエストなど様々な処理を行っている。
- クラス間の依存関係が複雑化している。
- 連想配列を多用しており、データの流れが分かりづらくなっている。
- 今後Moneytree連携に関する機能改修があるため、さらに複雑化する前に設計をスッキリさせたい。
2. 個人的な動機
- 自分がMoneytree連携周りの機能開発を担当することが多く、ドメイン知識が身についていたため。
- クリーンアーキテクチャの輪読会をちょうど終えたところなので、実践したいと考えたため。
クラス図と各クラスの役割
下記が「Moneytree連携」機能のリファクタリング後の設計になります。
↓各層ごとの色付けは、よく見かけるこのクリーンアーキテクチャの図から持ってきています。
出典:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
下記にそれぞれのクラスの役割などを解説します。
Controller
- Usecase層の呼び出し口として機能します。
- Controllerでは直接ビジネスロジックを処理しない。
ViewModel
- MVVMパターンで言うところのViewModel。Presenterに近い位置付けの役割を担う。
- Usecase層から返されるデータをフロントエンドが欲しい形式に整えることで、フロントエンドをビジネスロジックから分離できる。
Model
- DBに対してクエリを実行し、データを連想配列として取得。
- 必要に応じてDTOに変換して利用しているが、現状DTOへの変換処理はビジネスロジックに別途実装されている。
Gateway
- Moneytreeの仕様に合わせてAPIリクエストを行う。
- Moneytreeの仕様に則ってレスポンスデータ・リクエストデータをフォーマットする。
- いわばSDKのようなもの。 APIリクエスト用モジュールの実装例:
<?php /** * 「法人口座」系APIのリクエストを行うクラス */ class Gateway_Account_Requester extends Client implements Gateway_Account_IRequester{ /** * @see Gateway_Account_IRequester */ public function get_corporate_accounts(string $token, int $page): GetCorporateAccounts_Response { //requestはAPIリクエストを行うメソッド(Clientに定義) $response = $this->request( self::HTTP_METHOD_GET, Endpoint::GET_COURPORATE_ACCOUNTS, $token, '', [], ['page' => $page] ); return new GetCorporateAccounts_Response($response); } }
Handler
- Gatewayに実装されているAPIリクエスト用のモジュールを用いて、ロボのユースケースに合わせてAPIリクエストを行う。 Handlerの実装例:
<?php /** * Moneytreeアカウントに紐づくMoneytree法人口座を全件取得するHandler */ class Handler { /** @var \\IRequester */ private \\IRequester $requester; /** * コンストラクタ */ public function __construct(\\IRequester $requester) { $this->requester = $requester; } /** * Moneytree法人口座を全件取得 * @return void */ public function handle(): Handler_OutputData { $total_accounts = []; $page = 1; while (TRUE) { $response_accounts = $this->requester->get_corporate_accounts($token, $page++)->accounts; $this->accounts = array_merge($total_accounts, $response_accounts); if (sizeof($response_accounts) === 0) break; } } }
Service
- HandlerとModelをハンドリングする
- ServiceとHandlerはUsecase層であり、ServiceはUsecase層のとりまとめみたいな役割をする。
課題と考慮事項
今回のリファクタリングで「Moneytree連携」に関するモジュール群はかなりスッキリしましたが、実はまだまだ課題があります。備忘録的に書いていこうと思います。
Gateway・Handlerの配置
Gatewayは現在SDKのように機能しており、Frameworks & Driver層(青)とInterface Adopter層(緑)の間に位置しています。そう考えると、HandlerもInterfaceを設けてInterface Adopter層(緑)内に配置されるのが正しいように感じます。
Modelの改善
現在、モデルはデータを連想配列として返し、それをDTOに変換しています。理想的には、Repositoryパターンを実装してEntityを直接取得するようにすべきです。これにより、データアクセスロジックの明確さと保守性が向上します。 Repositoryもまた、Gatewayと同様にFrameworks & Driver層(青)とInterface Adopter層(緑)の間に位置するので、RepositoryをハンドリングするInterfaceAdopter層(緑)に位置付けられるモジュールが必要です。
展望
上記の課題感を踏まえると、最終的に下記のような設計になると良さそうです。
- リポジトリ層を追加→エンティティを返すように
- Handlerで担っていた役割をGatewayに移行(プロダクトのユースケースに合わせてAPIリクエストする)
- Usecase層がスッキリした
感想とまとめ
良かった
- 各層のインプットとアウトプットがオブジェクト化されたので、データの流れが追いやすくなった
- 処理の流れもわかりやすくなった
- 実装力がついた
- クラスの責務が細分化されているのでテストが書きやすい 難しかったこと
- 実装量が多いので結構時間かかる(→ただ今後は横展開して行くだけなので労力は低いはず)
- アーキテクチャに対する理解が甘かった
- 気づかないうちにアンチパターン的なことをしていた
- 当初層は展望の方のクラス図に近かったが、Entityがない状態で実装したため、アウトプットとインプットがごちゃついた印象になってしまった。
今回のリファクタリングは、難しいところこそ多かったものの、複雑化したモジュールを綺麗にすることができたのと、個人的にはアーキテクチャに対する理解を深めたり、実装力をつける良い機会になったので良かったなと思いました!
We are hiring!!
ROBOT PAYMENTでは一緒に働く仲間を募集しています!!!