「なんだかこのクラスめっちゃ触るの嫌だよね」
という状況よくあると思います。
(もし無いなら良い環境にいらっしゃるんだと思います)
いろんな現場先でいろんなコードを見てきましたが、
非常に修正コストのかかるプログラムがあるのはどこに行っても常にありました。
今回、縁があってそんなプログラムの改善活動をさせていただけたので備忘録的に残しておこうと思います。
フレームワーク的にはLaravelですが他のフレームワークにも応用効く内容じゃかなと思ってます。
(前提の話)FormRequestってそもそもなんだっけ?
今回の改善対象であるFormRequestクラスについてまずは軽く触れさせてください。
Laravelにはリクエスト内容をバリデーションする仕組みとしてFormRequestを提供しています。
詳しくは公式やreadouble見てもらいたいのですが、
要はバリデーションに関するルールなどをFormRequestをextendsしたクラス内で行うことで
責務を分割しようという仕組みです。
FormRequestをextendsしたクラスはバリデーションに専念することができるし、
Controllerはバリデーション後のリクエスト内容を扱えるようになるため、
スッキリとすることができます。
以降の記事の内容ですが
- 便宜上、FormRequestをextendsしたクラスのことをFormRequestクラスと呼ぶこととします。
- 手元にソースがなくて雰囲気で書いているのでコードのところは大いに間違ってる可能性があります。
改善を行いたいクラス状況
本題。
リクエストに対してバリデーションを行うFormRequestクラスですが、
このクラスがめっっっっちゃ肥大化してました。
FormRequestクラスだけで1000行とか軽く超えていました。
冒頭にあった「このクラス触るのめっちゃ嫌だよね」状態です。
なんでこんなことになったのでしょうか?
いろんな複合的な要因もあると思いますが、最もシンプルな原因は
画面で要求される項目数が多いことです。
対象となる画面はいわゆる管理系の画面でして「1画面でなんでも行いたい」みたいな要望を叶える形で実装されてました。
ので、100以上の項目を含む巨大な画面となってました。
(ちゃんと数えてないけどもっとあったかも)
業界的な要因もあって項目追加や修正などは頻繁にあり、
その度にFormRequestクラスをいじらないといけないのは非常にストレスのかかる仕事でした。
改善するにあたって決めたルール
肥大化したFormRequestクラスをなんとかするにあたって心掛けたルールは以下の3点です。
ルール1 FormRequestクラスの肥大化をこれ以上させないこと
まず大前提。
今後項目追加があってもFormRequestクラスに対してルールを追加しなくて済むようにしたいです。
withValidatorメソッドでの処理も複雑だったのでそこも同様です。
ルール2 UTを導入できること
バリデーション自体はLaravelのValidatorのおかげでシンプルに記載をすることができますが、変更後に従来のバリデーションを壊していないかを確認できるようにしたいです。
項目追加した度に手動テストは非常に辛いんです。。。
ルール3 バリデーションロジックを使いまわせること
「同じようにバリデーションして欲しい」という画面は多岐に渡り、
現状はベースとなるFormRequestクラスを継承してルール調整が行われてました。
元々がカオスなところにさらにルールが追加されるのですからコード読むしんどさはさらに増します。
この状況を打破すべく、バリデーションロジックを使いまわしやすい形を目指しました。
改善方法
Step1 項目をグルーピングする
例えばユーザーの情報編集画面があったとします。
画面全体としてはユーザー情報の編集を行うことを目的としていますが、
項目毎に見ていくといくつかにグルーピングできることが多いと思います。
適当に考えてますがこんなイメージです。
- ユーザー
- 氏名情報(苗字、名前、ふりがな、など)
- 住所情報(郵便番号、都道府県、番地、など)
- 勤務地(企業名、企業所在地、入社してからの年数、など)
こうした意味単位のグルーピングを行い、それらをStep2のクラスに落とし込んでいきます。
Step2 バリデーションを実行する責務のみを持つクラスValidationExecutorの作成
バリデーションのみを行うことに特化したValidationExecutorクラスを作成します。
Step1の氏名情報のバリデーションを行うクラスならこんなイメージです。
class UserNameValidationExecutor{
public function validate(array $input){
$validator = Validator::make($input,[苗字や名前のルールなんかを書く]);
return $validator->getMessageBag();
}
}
このValidationExecutorに対してUTを書くことにより「ルール2 UTを導入できること」を達成することができます。
Step3 バリデーションとパラメーターのマッピングを管理するValidationManagerの作成
個々のグループに対するバリデーションロジックを作ることはできましたが
- Step2で作ったValidationExecutorはリクエスト構造を無視してできるだけシンプルに作っている
→ リクエスト構造と調整を行わないといけない - 通常、1つのValidationExecutorだけで画面のバリデーションは完結しない
→ 個別のValidationExecutorを組み合わせて利用したい
という問題が残っています。
この問題を解決するためにValidationExecutorを統括するためのクラスValidationManagerを作成していきます。
一つずつ見ていきましょう。
リクエスト構造と調整を行わないといけない
リクエストパラメーターとValidationExecutorのルールは現状パラメーター名が乖離している状態です。
リクエストのjsonイメージはこんな感じ。
{
"user_id" : 1,
"user_first_name" : "太郎",
"user_last_name" : "テスト",
}
一方ValidationExecutorのルールはこんな感じ。
[
'id' => 'required|integer',
'first_name' => 'required|string',
'second_name' => 'required|string',
]
これらの差分を吸収するためにValidationManagerクラスを作成します。
例えば氏名情報のバリデーション時の差分を吸収するならこんな感じ。
class UserValidationManager{
public function validate(array $input){
$userNameValidationExecutor = new UserNameValidationExecutor();
$messages = $userNameValidationExecutor->validate($this->adjustUserNameInput($input));
return $this->adjustUserNameError($messages);
}
private function adjustUserNameInput(array $input){
$adjustedInput = [];
$adjustedInput['id'] = $input['user_id'];
$adjustedInput['first_name'] = $input['user_first_name'];
$adjustedInput['second_name'] = $input['user_last_name'];
return $adjustedInput;
}
private function adjustUserNameError(MessageBag $messages){
$newMessages = new Illuminate\Support\MessageBag;
foreach($messages->errors()->all() as $key => $message){
$newMessages->put('user_'.$key,$message)
}
return $newMessages;
}
}
個別のValidationExecutorを組み合わせて利用したい
↑に記載したコードでなんとなく察しがつくかと思いますが、ValidationManagerはExecutorをまとめる役割を持たせることもできます。
なので、使いたいExecutorが複数ある場合はManager内のvalidate
メソッドにてnewしてやればいくらでも対応できるという流れになります。
「ルール3 バリデーションロジックを使いまわせること」はこのManagerクラスによって実現できます。
Step3 ControllerからValidationManagerを呼び出す
ここまで来ればあとは簡単です。
今まで使っていたFormRequestクラスを捨て去って、コントローラーからValidationManagerを呼び出してしまいましょう。
「ルール1 FormRequestクラスの肥大化をこれ以上させないこと」が実現できました。
実際のプロジェクトではすでに肥大化してしまっているFormRequesクラスを捨て去ることができないので、
FormRequestクラスのwithValidator内でManagerクラスを呼び出しています。
なんとなくのクラス図でおさらい
最終的にこんな感じで利用することができます。
複数のコントローラーに跨って使用したいルールがあってもManagerを経由することで呼び出せばいろんな場面で使いまわせるという形になっています。
まとめ
LaravelのRequestFormの仕組み自体はいいものだなと思いますが、
フレームワークでカバーしきれない業務要件がある時にどう改善していくのかという事柄を考える良いきっかけとなりました。
「なんだかこのクラスめっちゃ触るの嫌だよね」の感覚と、
その状態をどうやって改善していくか考えることがエンジニアの仕事の一つだと思いますのでこれからも頑張ってお仕事していきたいなと思いました。