1. ホーム
  2. programming-languages

[解決済み] なぜ'if'文は悪とされるのか?

2023-05-13 06:15:32

疑問点

私は今 シンプルデザイン&テスト会議 . あるセッションで、プログラミング言語における悪のキーワードについて話していました。 コーリー・ヘインズ は、このテーマを提案し、次のように確信していました。 if ステートメントが絶対悪であると確信していました。 彼の代案は 述語 . なぜ if は悪なのか説明してください。

を悪用した非常に醜いコードを書くことができることは理解しています。 if . しかし、私はそれがそれほど悪いことだとは思っていません。

どのように解決するのですか?

この if ステートメントが、以下のように邪悪であるとみなされることはほとんどありません。 goto やミュータブルグローバル変数と同じくらい悪とみなされることはほとんどなく、後者でさえも実は普遍的かつ絶対的に悪ではありません。私は、この主張を少し誇張したものとして受け取ることをお勧めします。

それはまた、プログラミング言語と環境にも大きく依存します。パターンマッチングをサポートする言語では、(例えば) if を自由に置き換えることができます。しかし、C言語で低レベルのマイクロコントローラーをプログラミングしているのであれば、の代わりに if を関数ポインタに置き換えることは、間違った方向への一歩になるでしょう。そこで、ほとんどを置き換えることを検討します。 if をOOPプログラミングで使うのは、関数型言語では if は慣用句ではありませんし、純粋に手続き型の言語では、そもそも他の選択肢があまりないからです。

とはいえ、条件節は時に管理しにくいコードになります。これには if ステートメントだけでなく、より一般的には switch ステートメントよりも多くの分岐を含んでいます。 if よりも多くの分岐を含んでいます。

を使用するのが完全に合理的な場合もあります。 if

ユーティリティメソッドや拡張機能、特定のライブラリ関数を書いている場合、どうしても if を避けることはできないでしょう (そして、避けるべきではありません)。この小さな関数をコード化するのに良い方法はありませんし、これ以上自己文書化することもできません。

// this is a good "if" use-case
int Min(int a, int b)
{
    if (a < b) 
       return a;
    else
       return b;
}

// or, if you prefer the ternary operator
int Min(int a, int b)
{
    return (a < b) ? a : b;
}

タイプコードへの分岐はコード臭の原因になります。

一方、ある種の型コードをテストする、あるいは変数が特定の型であるかどうかをテストするコードに遭遇した場合、これはリファクタリングの良い候補である可能性が高い、すなわち 条件式をポリモーフィズムに置き換える .

この理由は、呼び出し側が特定の型のコードで分岐できるようにすることで、コード中に多数のチェックが散らばってしまい、拡張やメンテナンスがより複雑になってしまう可能性が出てくるからです。一方、ポリモーフィズムでは、この分岐の決定をできるだけプログラムの根幹に近づけることができます。

を考えてみましょう。

// this is called branching on a "type code",
// and screams for refactoring
void RunVehicle(Vehicle vehicle)
{
    // how the hell do I even test this?
    if (vehicle.Type == CAR)
        Drive(vehicle);
    else if (vehicle.Type == PLANE)
        Fly(vehicle);
    else
        Sail(vehicle);
}

共通だが型固有の(つまりクラス固有の)機能を別々のクラスに配置し、仮想メソッド(またはインターフェース)を通じて公開することで、プログラムの内部ではこの決定を呼び出し階層の高い人(コード内の一箇所でも可能)に委ねることができ、テスト(モッキング)、拡張性、保守性がずっと容易になります。

// adding a new vehicle is gonna be a piece of cake
interface IVehicle
{
    void Run();
}

// your method now doesn't care about which vehicle 
// it got as a parameter
void RunVehicle(IVehicle vehicle)
{
    vehicle.Run();
}

そして、これで簡単に RunVehicle メソッドが正常に動作しているかどうかを簡単にテストすることができます。

// you can now create test (mock) implementations
// since you're passing it as an interface
var mock = new Mock<IVehicle>();

// run the client method
something.RunVehicle(mock.Object);

// check if Run() was invoked
mock.Verify(m => m.Run(), Times.Once());

のみが異なるパターン。 if の条件だけが異なるパターンを再利用することができます。

を置き換えることについての議論について if を述語に置き換えるという議論についてですが、ヘインズ氏はおそらく、条件式だけが異なる類似のパターンがコード上に存在することがあることを述べたかったのでしょう。条件式は if と一緒に出てきますが、全体のアイデアは、繰り返されるパターンを別のメソッドに抽出し、式をパラメータとして残すことです。これはLINQがすでに行っていることです。 通常 に比べ、よりきれいなコードになります。 foreach :

この2つの非常によく似た方法を考えてみましょう。

// average male age
public double AverageMaleAge(List<Person> people)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (person.Gender == Gender.Male)
       {
           sum += person.Age;
           count++;
       }
    }
    return sum / count; // not checking for zero div. for simplicity
}

// average female age
public double AverageFemaleAge(List<Person> people)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (person.Gender == Gender.Female) // <-- only the expression
       {                                   //     is different
           sum += person.Age;
           count++;
       }
    }
    return sum / count;
}

これは、条件を述語に抽出することで、これら2つのケース(および将来の他の多くのケース)に対して1つのメソッドを残すことができることを示しています。

// average age for all people matched by the predicate
public double AverageAge(List<Person> people, Predicate<Person> match)
{
    double sum = 0.0;
    int count = 0;
    foreach (var person in people)
    {
       if (match(person))       // <-- the decision to match
       {                        //     is now delegated to callers
           sum += person.Age;
           count++;
       }
    }
    return sum / count;
}

var males = AverageAge(people, p => p.Gender == Gender.Male);
var females = AverageAge(people, p => p.Gender == Gender.Female);

そして、LINQにはすでにこのような便利な拡張メソッドがたくさんあるので、実は独自のメソッドを書く必要さえないのです。

// replace everything we've written above with these two lines
var males = list.Where(p => p.Gender == Gender.Male).Average(p => p.Age);
var females = list.Where(p => p.Gender == Gender.Female).Average(p => p.Age);

この最後のLINQバージョンでは if ステートメントは完全に消滅してしまいましたが。

  1. は、正直なところ、問題は if それ自体ではなく、コードパターン全体(単に重複していたため)であり
  2. if は実際に存在しますが、LINQの内部で書かれています。 Where 拡張メソッド内に書かれており、テストされ、修正も終了しています。自分自身のコードが少ないことは常に良いことです。テストすることが少なくなり、うまくいかないことが少なくなり、コードはフォロー、分析、保守がよりシンプルになります。

ネストされた巨大な if / else ステートメント

1000行に及ぶ関数があり、数十のネストされた if ブロックがある場合、次のように書き換えることができる可能性が非常に高いです。

  1. より良いデータ構造を使用し、より適切な方法で入力データを整理する(例えばハッシュテーブル、これは一回の呼び出しで一つの入力値から別の入力値にマッピングする)。
  2. 10 行以下で同じロジックを実行する数式やループ、場合によっては既存の関数を使用する (例. この悪名高い例 が思い浮かびますが、一般的な考え方は他のケースにも当てはまります)。
  3. ガード句を使用して入れ子を防ぐ (ガード句は例外的なケースをできるだけ早く取り除くので、関数全体の変数の状態に対してより信頼性が高くなります)。
  4. で置き換え、少なくとも switch 文に置き換えてください。

コード臭いと感じたらリファクタリングするが、過剰なエンジニアリングはしない。

ここまで言っておいてなんですが、あなたは は眠れぬ夜を過ごすべきではありません。 を使う必要はありません。これらの回答は一般的な経験則を提供しますが、リファクタリングを必要とする構造を検出することができる最良の方法は、経験を通じてです。時間が経つにつれて、同じ節を何度も修正することになるいくつかのパターンが浮かび上がってきます。