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