Почему утверждение «если» считается злом? - PullRequest
60 голосов
/ 12 октября 2009

Я только что пришел с Конференция по простому проектированию и тестированию . На одной из сессий мы говорили о злых ключевых словах в языках программирования. Кори Хейнс , который предложил тему, был убежден, что if утверждение является абсолютным злом. Его альтернативой было создание функций с предикатами 1007 *. Не могли бы вы объяснить мне, почему if является злом.

Я понимаю, что вы можете написать очень уродливый код, злоупотребляющий if. Но я не верю, что это так плохо.

Ответы [ 18 ]

83 голосов
/ 12 октября 2009

Есть еще один смысл, в котором if может быть злом: когда оно приходит вместо полиморфизма.

Е.Г.

 if (animal.isFrog()) croak(animal)
 else if (animal.isDog()) bark(animal)
 else if (animal.isLion()) roar(animal)

вместо

 animal.emitSound()

Но в основном, если это совершенно приемлемый инструмент для того, что он делает. Конечно, им можно злоупотреблять и злоупотреблять, но это далеко от статуса goto.

74 голосов
/ 12 октября 2009

Условные предложения иногда приводят к появлению кода, которым сложнее управлять. Это включает не только оператор if, но даже чаще оператор switch, который обычно включает больше ветвей, чем соответствующий if.

В некоторых случаях вполне разумно использовать if

Когда вы пишете служебные методы, расширения или специальные библиотечные функции, вполне вероятно, что вы не сможете избежать if s (и не должны). Нет лучшего способа закодировать эту маленькую функцию и сделать ее более документированной, чем она есть:

// 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 s, но вся идея состоит в том, чтобы выделить повторяющийся шаблон в отдельный метод, оставив выражение в качестве параметра. Это то, что LINQ уже делает, обычно , что приводит к более чистому коду по сравнению с альтернативным foreach:

Рассмотрим два очень похожих метода:

// 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;
}

Это означает, что вы можете извлечь условие в предикат, оставив вам один метод для этих двух случаев (и многих других будущих случаев):

// 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, который был протестирован и закрыт для модификации. Полезно иметь меньше собственного кода: меньше вещей для тестирования, меньше ошибок, а код проще отслеживать, анализировать и поддерживать.

Рефакторинг, когда вы чувствуете, что это запах кода, но не перегружайте

Сказав все это, вы не должны проводить бессонные ночи из-за того, что сейчас и там есть пара условий. Хотя эти ответы могут предоставить некоторые общие практические правила, лучший способ уметь обнаруживать конструкции, требующие рефакторинга, - это через опыт. Со временем появляются некоторые шаблоны, которые приводят к изменению одних и тех же предложений снова и снова.

32 голосов
/ 12 октября 2009

Хорошая цитата из Code Complete:

Кодируйте, как если бы кто-то поддерживал вашу программу, это насильственный психопат, который знает, где ты живешь.
& mdash; Anonymous

IOW, будь проще. Если удобочитаемость вашего приложения будет улучшена с помощью предиката в определенной области, используйте его. В противном случае используйте «если» и продолжайте.

17 голосов
/ 12 октября 2009

Я думаю, это зависит от того, что вы делаете, чтобы быть честным.

Если у вас есть простое выражение if..else, зачем использовать predicate?

Если вы можете, используйте switch для более крупных if замен, а затем, если есть возможность использовать предикат для больших операций (где это имеет смысл, иначе ваш код будет кошмаром для обслуживания), используйте его .

Мне кажется, этот парень немного педантичен. Замена всех if предикатами - просто сумасшедший разговор.

14 голосов
/ 12 октября 2009

Существует кампания Anti-If , которая началась в начале года. Основная предпосылка заключается в том, что многие вложенные операторы часто могут быть заменены полиморфизмом.

Мне было бы интересно увидеть пример использования предиката. Это больше похоже на функциональное программирование?

10 голосов
/ 12 октября 2009

if не является злом (я также считаю, что присвоение морали практикам написания кода - глупость ...).

г. Хейнс глуп, и над ним надо смеяться.

9 голосов
/ 12 октября 2009

Также как в библейском стихе о деньгах, если утверждения не являются злом, - ЛЮБОВЬ, если заявления - это зло. Программа без утверждений if является нелепой идеей, и использование их по мере необходимости имеет важное значение. Но программа, имеющая 100 блоков if-else if подряд (что, к сожалению, я видел), безусловно, злая.

8 голосов
/ 12 октября 2009

Я должен сказать, что недавно я начал рассматривать, если операторы пахнут как код: особенно когда вы повторяете одно и то же условие несколько раз. Но есть кое-что, что вам нужно понять о запахах кода: они не обязательно означают, что код плохой. Они просто означают, что есть хороший шанс код плохой.

Например, комментарии Мартина Фаулера перечислены как запах кода, но я бы не воспринял всерьез никого, кто бы сказал, что «комментарии - это зло, не используйте их».

Как правило, я предпочитаю использовать полиморфизм вместо операторов if, где это возможно. Это просто делает намного меньше места для ошибок. Я склонен находить, что во многих случаях использование условных выражений также приводит к большому количеству аргументов бродяг (потому что вы должны передать данные, необходимые для формирования условного выражения, соответствующему методу).

7 голосов
/ 12 октября 2009

Я с тобой соглашусь; он был не прав . Вы можете зайти слишком далеко с такими вещами, слишком умными для вашего же блага.

Код, созданный с использованием предикатов вместо ifs, будет ужасно поддерживать и тестировать.

4 голосов
/ 12 октября 2009

Предикаты происходят из логических / декларативных языков программирования, таких как PROLOG. Для определенных классов задач, таких как решение ограничений, они, возможно, превосходят многие пошаговые «если это делать то, что потом делают». Задачи, которые долго и сложно решать на императивных языках, можно решить всего за несколько строк в PROLOG.

Существует также проблема масштабируемого программирования (из-за перехода к многоядерности, сети и т. Д.). Если утверждения и императивное программирование в целом имеют тенденцию быть в пошаговом порядке и не масштабируемы. Тем не менее, логические декларации и лямбда-исчисление описывают, как можно решить проблему и на какие части ее можно разбить. В результате интерпретатор / процессор, исполняющий этот код, может эффективно разбить код на части и распределить его по нескольким процессорам / ядрам / потокам / серверам.

Определенно не везде полезен; Я не хотел бы пытаться написать драйвер устройства с предикатами вместо операторов if. Но да, я думаю, что главное - это, вероятно, звук, и стоит хотя бы ознакомиться с ним, если не использовать все время.

...