こんにちは、バックエンドエンジニアの青山です。今回はデザインパターンをLaravelのプロジェクトに適用してみました。
実際にありそうな仕様を想定して、ナイーブな実装からデザインパターンを使った実装にリファクタリングする形で進めていこうと思います。
たとえば
- ユーザーが単発の仕事に応募してその報酬をもらえるようなサイトがあるとする
- 仕事情報は管理画面からCSVでアップロードする仕様になっており、アップされたCSVの内容はDBに保存される
- 仕事にはいくつかの種類があり、扱うデータが異なる
- ↑の理由からCSVのフォーマットは仕事の種類ごとに異なる
- 管理画面にあるCSVアップロードフォームは1つで、フォーム内にはファイル用inputとファイルのタイプ(どんなジャンルの仕事が記載されているCSVなのか)を選択するselectが存在する
みたいな仕様があったとします。
フォームはこんな感じのものを想像してください。
仕事のジャンルはいくつもある想定ですが、ここでは一旦「オフィスワーク」「運送」「飲食」の3つが存在するとします。このどれかをselectで選択して、そのジャンルの仕事情報のCSVをアップロードするというわけです。
一旦そのまま仕様を満たしてみるとして、一番ナイーブな実装はこんな感じだと思います。
リクエストの値を見てifで条件分岐させる単純な作りです。これをデザインパターンを適用してリファクタリングしてみます。
デザインパターンの適用
今回使用するのは
- Strategyパターン
- Factory Methodパターン
この2つです。
Strategyパターン
状況に応じて処理を動的に切り替えることを可能にするパターンです。それぞれの処理をクラスとして定義し、共通の呼び出し部分から呼び出して処理を代替できるようにします。
今回の場合だと、"CSVを読み込んでDBにデータを保存する"という処理をそれぞれの種類で分けてクラス化することで実現します。
各Strategyの実装
まずそれぞれのクラスの共通のインターフェースを作成します。データをやりとりするためのDtoクラスもついでに作成しておきます。
CSVのデータを読み込んで配列を返すメソッドと、データをDBに保存するメソッドの2つを定義しました。実装内容は各実装クラスに委ねることになります。
これらのクラスが図中の各ConcreteStrategyに相当します。
これで具体的な処理を行うクラスを実装できました。次に必要なのはこれらのどのクラスを使用するか決めるクラス、上の図のContextに相当する部分です。今回の場合だと、フォームのselectで選択された値が何であるかによってどのクラスをインスタンス化するか判別する処理となります。
ifやswitchを使用して条件に引っかかれば指定のクラスをインスタンス化するという方法でもいいかもしれませんが、今回はここでもう一つデザインパターンを適用してみたいと思います。
Factory Methodパターン
共通のインターフェースを実装したいくつかのクラスを作成し、どのクラスをインスタンス化するかをサブクラスに任せるパターンです。インスタンス生成処理と使用部分を分割することでメンテナンスしやすい状態にできるというメリットがあります。
インスタンス生成用クラスの実装
まず、Strategyパターン図中のConcreteStrategyに相当するクラスをインスタンス化するクラスを作成します。
インスタンス化される条件はリクエストのfile_typeの値が指定の値であった場合です。この条件判定を実現するために、FileImporterInterfaceに新しくtypeというメソッドを追加します。
おのずと各実装クラスもtypeメソッドを持たなければならなくなるので、修正します。
このような固有の数値を返すだけのメソッドを、残りの各クラスにも実装します。このtypeメソッドを使ってImporterFactoryクラスで条件判定とインスタンス生成を行います。
条件に応じて処理を切り替えるクラスの実装
Strategyパターン図中のContextにあたる部分です。インスタンスの作成はImporterFactoryが担っているので、かなりスッキリした作りになりました。
JoblistFileImportContextの呼び出し
こちらの処理をどこに置くかは好みが分かれると思います。これぐらいならControllerに書いてもよさそうな気がしますが、ビジネスロジック用にJoblistFileImportServiceクラスを作成してそちらに配置することにします。
この処理をコントローラーから呼び出します。
FormRequest
コントローラーを実装する前にFormRequestを作成します。このクラス内でバリデーションとリクエスト値をFileImportDtoに変換する処理を行います。
Controller
最後にコントローラーからJoblistFileImportServiceの処理を呼び出して完成です。とてもミニマルなコントローラーになりました。
メリット
最初に提示したifでの分岐による実装では、種別が増えるたびにどんどん分岐が追加されていくので、見通しが悪くなってしまうというデメリットがありました。
リファクタリング後の実装では種別が増えても新しくクラスを追加してImporterFactoryのimportersプロパティにクラスを登録するだけで済むので、見通しが良くメンテナンスしやすい状態になりました。
ちなみに元ネタとなる記事をちょっと前にQiitaに投稿していますので、よければそちらもご覧ください。
さいごに
Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。