C#における条件付きifのリファクタリング:デレク・コマーティンと条件分岐を回避する
C#では、if文、if else文、switch文などの条件文が不可欠なツールです。 しかし、これらの構成要素が過度に使用された場合、特に列挙型に結びついた場合はどうなるのでしょうか? デレク・コマーティンは、ビデオ"Enums Aren't Evil. Conditionals Everywhere Are"では、広範な条件ロジックを、よりクリーンで保守性の高いパターンに置き換える詳細なリファクタリングを紹介します。
この記事では、デレクのタイムスタンプをアンカーとして使いながら、デレクの推論を順を追って説明します。 また、三項演算子、else文、switch-case構造など、C#の一般的な条件パターンに彼のアイデアがどのように適用されるかを探求し、これらが大規模なコードベースで問題を引き起こす可能性がある場所と、より良い設計に向けてどのようにリファクタリングできるかを明らかにします。
条件Ifの爆発:本当の問題
Derekは、製品の種類をチェックするif文の紹介から始めます:
if (productType == ProductType.Template || productType == ProductType.Ebook)if (productType == ProductType.Template || productType == ProductType.Ebook)一見すると、上記の条件付きifは簡単そうに見えます。 しかしデレクは、この種の文は与えられた条件を評価し、その条件が真である場合にのみコードのブロックを実行する。
このブロックは、別のメソッドやクラスで再び登場するかもしれません:
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)デレクは、このパターンは大規模なシステム全体にすぐに広がると説明する。 複数のサービスで同じ if else ロジックが表示され、新しい列挙値を追加する際に矛盾やバグが発生します。 たとえば、Video のような新しい製品タイプを導入する場合はどうなりますか? この条件式が存在するすべてのブロックを忘れずに更新する必要があります。
繰り返しは複雑さを増幅します
次の例では、デレクが入れ子になった条件式について掘り下げています。 あるメソッドの内部で、if else文が同じ列挙型をチェックし、その結果はさらに別のメソッドに渡されます。
このステートメントは、TemplateかEbookをチェックし、何かを返します。 デレクは、このような冗長性はコードを長くするだけでなく、メンテナンスの危険性をもたらすと指摘する。 同じロジックが複数のファイルにまたがってコピーされるため、制御フローが混乱します。
もしあなたのシステムが、列挙型に触れるたびにデフォルトの大文字と小文字を追加しなければならないのであれば、何かが間違っていることがわかります。
条件式についての考え方を変える
常にif elseで型をチェックする代わりに、デレクはより良い質問をすることを提案する:
製品はダウンロード可能ですか?
これは、より良い意思表示です。 コードをより読みやすくし、enumへの依存を完全に減らします。 のような2つの条件を含むif文を書くのではなく、以下のようにします:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)そのロジックをモデルにカプセル化して書くだけです:
if (product.HasDownloadableResource())if (product.HasDownloadableResource())これは、ダウンロード可能なリソースが存在する場合にのみ真を返し、複雑な条件式の必要性を低減します。
Ifステートメントからカプセル化された動作へ
核心的な問題を解決するために、DerekはDownloadableResource型を導入しました。 このタイプには、ダウンロードURLとデフォルトのファイル名が含まれます。 if文に頼るのではなく、ドメインの第一級の一部となります。
さて、これを繰り返す代わりに
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}こう書いてください:
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}これはロジックを劇的に単純化し、else文の分岐やswitch文さえも不要にします。
コンパイル時よりもランタイム: 戦略的転換
デレクはさらに、重要な設計上の選択である、ロジックをコンパイル時からランタイムに移行することについて説明している。これは、ある製品にDownloadableResourceが存在するかどうかを、実行時にシステムに問い合わせることを意味します。 そうであれば、行動してください。 そうでない場合は、飛ばしてください。
彼は、この動きは静的なif elseロジックをランタイムクエリに変えると指摘している。 データベースの呼び出しが追加されるかもしれませんが、入れ子になったif elseロジックを減らし、動作を一元化します。 これにより、規模に応じた保守性が向上します。
ダウンロード可能な製品のために継承を使用する
デレクが模索するもう1つのルートは継承だ。 抽象的な基本クラスProductを作成し、Ebook、Template、OfflineCourseのような派生型を定義することができます。
それぞれ、次のようなメソッドをオーバーライドします:
public virtual string GetDownloadUrl() { ... }public virtual string GetDownloadUrl() { ... }このアプローチでは、各製品が独自のロジックを扱うことができます。 これはswitch文や複数の条件文を避けるものですが、それでも注意しないと内部で条件式を書いてしまう可能性があることをデレクは指摘しています。
継承なしでより良いカプセル化を実現する
継承が強引に感じられる場合は、DownloadableProductのような明示的な型を使用することをお勧めします。
あなたのプログラムでは、次のようになります:
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());動作を決定するためにif else文やswitch文は必要ありません。
軽量修正: 列挙型の拡張メソッド
もしあなたが列挙型を放棄する準備ができていないのであれば、Derekは軽量な解決策を提案します:
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}今、書く代わりに
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)以下のように簡略化できます:
if (product.Type.IsDownloadable())if (product.Type.IsDownloadable())これにより、ロジックが一元化され、中括弧やコードのブロックが何度も繰り返されることがなくなります。
三項演算子とスイッチの使いすぎに注意
Derekはまた、三項演算子のような省略記法を使いすぎないように注意しています:
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";有効な構文ではありますが、ロジックが複雑になるとエラーになりやすく、読みにくくなります。 特に、条件がfalseと評価される場合、微妙な方法で間違った値が代入される可能性があります。
同様に、break文とデフォルトのcaseを持つswitchもこの罠にはまる。スイッチ・ケース・ロジックを使うよりも、オブジェクトに動作を問い合わせた方がいい。
結論より少ない条件分岐でよりスマートに制御する
結論として、デレクのビデオは、列挙型を攻撃するものではなく、列挙型にまつわる条件付き if 構造の使用方法についての批評です。 if else文やswitch文をコードベース全体に広げることで、システムをテスト、保守、進化させることが難しくなります。
カプセル化、ランタイム・ルックアップ、継承、単純な拡張メソッドのどれを選んでも、ゴールは同じです。
覚えておいてください:
条件法も悪くない。
条件の乱雑さは
クリーンなコードは、クラスに散在する複数のif else文に依存しません。
- 文脈を評価し、それに応じてリファクタリングしてください。
デレクが言うように、"文脈次第 "です。しかし、ひとつ確かなことがあります。それは、製品は常に単なる製品ではないということです。時には、それはあなたのデザインを再考するシグナルになるのです。

