Enumとパターンマッチでコードの意図を明確にする
コードリーディングをしているときに、「この条件でこの関数を呼んでいるのは分かるが、なぜそうしているのかが分からない」と感じたことはありませんか?
そんなときに効果的なのが Enum と パターンマッチを使った書き方です。
Enumで「なにをしたいか(意図)」を名前として明示し、パターンマッチで「その意図に応じた処理」を分けることで、意図が明確なコードを書くことができます。
Enumとパターンマッチングを使った手法はよく知られていると思いますが、最近リファクタリングをする中で、整理できる場面に遭遇したので、実際にあったPHPのコードを例に紹介します。
元のコード
以下のコードは、外部システムと連携するための entity を作成している箇所です。
ModelはEloquentのModelのようなものを想像してください。
class EntitySyncAdapter
{
    function packEntity(Model $model, bool $dirty_mode)
    {    
        $entity = $this->getEntity($model);
        if (!$model->isLinkedExternalSystem()) {
            $entity->setAll($model);
        } else {
            if ($dirty_mode) {
                $entity->fillByDirtyData($model);
            } else {
                $entity->fillByModel($model);
            }
        }
        $this->addEntity($entity);
    }
}
このコードを見ると、条件に応じて適切な関数を呼んでいることは分かります。
しかし、「外部連携されていると何が違うんだろう、外部連携されているときにさらになんで分岐しているんだろう」などの理由は、コンテキストを深く知っていたり、コードを深く追わないと見えてきません。
この関数を読み解いていくと、
- 外部連携がまだされていない場合は新規作成として空の 
Entityを作り、setAllでセットする - すでに外部システムと連携されている場合は既存の 
Entityを取得し、dirty_modeに応じて差分更新 (fillByDirtyData) or フル更新 (fillByModel) 
というように、処理が複数のケースに分かれていることがわかりました。
この処理を( 命名や副作用がある関数を直したいという気持ちをグッと堪えて )、Enumとパターンマッチングを使ってリファクタリングしていきたいと思います。
リファクタリング例
enum SyncMode: string
{
    // 新規作成
    case CREATE = 'create';
    // 変更のあった項目のみ同期
    case PARTIAL_UPDATE = 'partial_update';
    // 全項目を再同期(変更の有無を問わず)
    case FULL_UPDATE = 'full_update';
}
class EntitySyncAdapter
{
    function determineSyncMode(Model $model, bool $dirty_mode)
    {
        if(!$model->isLinkedExternalSystem())
        {
            return SyncMode::CREATE;
        }
        return $dirty_mode ? SyncMode::PARTIAL_UPDATE : SyncMode::FULL_UPDATE;
    }
    function packEntity(Model $model, bool $dirty_mode)
    {    
        // まずは何をするか(SyncMode)を決める
        $sync_mode = $this->determineSyncMode($model, $dirty_mode);
        // そして mode に応じて実際の処理を振り分ける
        match ($sync_mode) {
            SyncMode::CREATE => $entity->setAll($model),
            SyncMode::PARTIAL_UPDATE => $entity->fillByDirtyData($model),
            SyncMode::FULL_UPDATE => $entity->fillByModel($model),
        };
        $this->addEntity($entity);
    }
}
「どの条件でどの処理が呼ばれるのか」がEnumとパターンマッチで明確に定義されます。これによって、意図がコードから読み取りやすくなります。
このコードでは、dirty_modeの意味や意図が呼び出し側に委ねられているため、さらに改善の余地はありますが、この記事では一旦ここまでとします。
意図を明確にするコツ
コツとしては、単に「〇〇の時は△△の関数を呼ぶ」と条件と処理を直接結びつけるのではなく、
「〇〇の時は、◻︎◻︎だから、△△の関数を呼ぶ」のように、間にある理由を考えてコードに落とし込むことだと思います。
先ほどの例では「外部連携されていない時は、新規作成だから、空のEntityに値を詰める」のように、なぜそうするのかを表現することが重要です。
この 「◻︎◻︎だから」 の部分こそが、コードにおける意図になります。その意図をEnumに落とし込み、 パターンマッチで分岐させることで、意図が明確なコードを書くことができます。
最後に
最近、X(旧Twitter)でも if / if-else のネストが話題になりましたが、このようにifやif-elseのネストが入り乱れている時には、意図が欠落しているケースが多いように思います。
なのでifやif-elseのネストが入り乱れているコードを見かけたら、Enumとパターンマッチを使って「なぜその処理をするのか?」をコードに表す工夫をしてみてください。
Discussion