😸

[良いコード悪いコード]条件分岐をきれいに書く

2025/01/13に公開

背景

施策の対応でちょい複雑なバリデーションロジックを実装することに。
元々if文地獄になっているバリデーション用のクラス...。
可読性をもう少し高めたいなあと思ったので久しぶりに「良いコード悪いコード」を読んでみた。

6章「条件分岐 - 迷宮かした分岐処理を解きほぐす技法」

今回は複雑な条件分岐の実装について書きたかったので、6章のみに焦点を当てます。
書籍に書いてあるchipsを以下にまとめます。

早期リターン

条件を満たさない場合にreturnで早期に抜けるようにする方法のこと。
if文が多くネストが深くなっている場合に使うと、ネストが浅くなって比較的読みやすくなる。

interface実装によるストラテジーパターン

各パターンをinterfaceで実装し、if文やswitch文の使用を回避する方法のこと。
例:図形に合わせて面積を取得したい場合

interface Shape
{
    function area();
}
// 図形:四角形
class Rectangle implements Shape
{
    private readonly double width;
    private readonly double height;
    function area()
    {
        return $this->width * $this->height;
    }
}
// 図形:円
class Circle implements Shape
{
    private readonly double radius;
    function area()
    {
        return $this->radius * $this->radius * 3.14;
    }
}
class Main
{
    function showArea(Shape $shape)
    {
        // if文で円と四角形の場合で分ける必要がなくなる
        return $shape->area();
    }
}

showArea(new Rectangle(10, 20));とすれば四角形の面積が得られます。

以下のような場合に有用(以下に限らずですが)

  • 同じ条件分岐が複数箇所で実装されている場合
  • メソッドの切り替えフラグが使用されている場合

今回実装の仕様

今回の改修で実装したかった仕様を説明します。
ざっくり以下のようなルールを実装する必要がありました。

  • 顧客の意思が「不賛成」の場合はステータスを完了にできない
  • 顧客が「未確認で完了にしてOK」と言っていない場合は、顧客の意思が「未確認」の状態でステータスを完了にできない

詳細な条件は以下です。
条件1〜3いずれかに当てはまる場合は保存できないようにする必要があります。

  • 条件1
    • 「ステータス」が「完了」
    • リクエスト値に「顧客の最新の意思」が含まれている
    • リクエスト値の「顧客の最新の意思」が「不賛成」
  • 条件2
    • 「ステータス」が「完了」
    • リクエスト値に「顧客の最新の意思」が含まれていない
    • DBから取得した「顧客の最新の意思」の値が「不賛成」
  • 条件3
    • 「ステータス」が「完了」
    • リクエスト値に「顧客の最新の意思」が含まれていない
    • DBから取得した「顧客の最新の意思」の値が「未確認」
    • 「顧客の未確認OKフラグ」の値が「可」以外

まずは普通に実装してみる

class validation
{
    public function 顧客の最新の意思チェック($hoge, $request, $validator)
    {
        if ($hoge->ステータス === 完了) {
            if ($request->input('顧客の最新の意思') !== null) {
                if ($request->input('顧客の最新の意思') === 不賛成) {
                    $validator->errors('不賛成の場合は完了にできません');
                }
            } else {
                $DBの顧客の最新の意思 = $hoge->DBの顧客の最新の意思取得();
                if ($DBの顧客の最新の意思 === 不賛成) {
                    $validator->errors('不賛成の場合は完了にできません');
                } elseif ($DBの顧客の最新の意思 === 未確認) {
                    if ($hoge->顧客の未確認OKフラグ !==) {
                        $validator->errors('未確認の場合は完了にできません');
                    }
                }
            }
        }
    }
}

ネストふけー!

ということでストラテジーパターンとやらで実装してみる

enum 顧客の最新の意思
{
    case 未確認 = 1;
    case 不賛成 = 2;
    case 賛成 = 3;
}

interface 顧客の最新の意思チェックインターフェース {
    public function 顧客の最新の意思チェック($hoge, $validator);
}

class 不賛成チェッククラス implements 顧客の最新の意思チェックインターフェース {
    public function 顧客の最新の意思チェック($hoge, $validator) {
        $validator->errors('不賛成の場合は完了にできません');
    }
}

class 未確認チェッククラス implements 顧客の最新の意思チェックインターフェース {
    public function 顧客の最新の意思チェック($hoge, $validator) {
        if ($hoge->顧客の未確認OKフラグ !==) {
            $validator->errors('未確認の場合は完了にできません');
        }
    }
}

class validation
{
    public function 顧客の最新の意思チェック($hoge, $request, $validator)
    {
        // バリデーションクラスをマッピング
        $validation_map = [
            顧客の最新の意思::未確認 => new 未確認チェッククラス(),
            顧客の最新の意思::不賛成 => new 不賛成チェッククラス(),
        ];

        if ($hoge->スタータス !== 完了) {
            // ステータスが完了以外の場合は早期リターン
            return;
        }

        $顧客の最新の意思 = 顧客の最新の意思::tryFrom(
            $request->input('顧客の最新の意思') ?? $hoge->DBの顧客の最新の意思取得()
        );
        if ($顧客の最新の意思 === null || !isset($validation_map[$顧客の最新の意思])) {
            // 例外パターン
            return;
        }

        $validation_map[$顧客の最新の意思]->顧客の最新の意思チェック($hoge, $validator);
    }
}

if文が減ってネストも浅くなり、バリデーションメソッドがだいぶすっきりした!
今後「顧客の最新の意思」に関係するバリデーションが追加された場合も、コードの修正が楽そう。
ちょっと手間はかかるけど。

感想

久しぶりに良いコード悪いコードを読むと意外と忘れてるところある。
たまに読み返そ。

参考

  • 仙塩大也. (2023). 改訂新版 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい、成長し続けるコードの書き方. 翔泳社. https://www.amazon.co.jp/dp/4297146223

Discussion