こんにちは、請求管理ロボ開発チーム所属の塚本です。
今回は、請求管理ロボの一部をリファクタリングし、クリーンアーキテクチャ化にした話について書いていこうと思います。
請求管理ロボでは、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では一緒に働く仲間を募集しています!!!