За-если антипаттерн


9

В этом блоге я читал об анти-паттерне «за-если», и я не совсем уверен, что понимаю, почему это анти-паттерн.

foreach (string filename in Directory.GetFiles("."))
{
    if (filename.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
    {
        return new StreamReader(filename);
    }
}

Вопрос 1:

Это из-за return new StreamReader(filename);внутри for loop? или тот факт, что вам не нужен forцикл в этом случае?

Как отметил автор блога, менее сумасшедшая версия этого такова:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
} 

оба страдают от состояния гонки, потому что если файл будет удален до создания StreamReader, вы получите File­Not­Found­Exception.

Вопрос 2:

Чтобы исправить второй пример, вы бы переписали его без оператора if, а вместо этого окружили StreamReaderблок try-catch, и если он бросил, File­Not­Found­Exceptionвы соответственно обработали его в catchблоке?



1
@amon - Спасибо, но я не делаю никаких дополнительных замечаний, я просто пытаюсь понять, о чем говорится в статье, и как я могу это исправить.

3
Я бы не стал так много думать. Постер блога составляет какой-то идиотский код, который никто не пишет, дает ему имя и называет его анти-паттерном. Вот еще один: «Я называю это бессмысленным шаблоном назначения: x = x; я вижу, что это делается постоянно. Так глупо. Думаю, я напишу об этом в блоге».
Мартин Маат

4
To fix the second example, would you re-write it without the if statement, and instead surround the StreamReader with a try-catch block, and if it throws a File­Not­Found­Exception you handle it in the catch block accordingly?- Да, именно это я и сделаю. Решение состояния гонки важнее, чем какое-то понятие «исключения как поток управления», и оно решает это элегантно и чисто.
Роберт Харви

1
Что делать, если ни один из файлов не соответствует заданным критериям? Это возвращается null? Обычно вы можете использовать LINQ для очистки кода, который выглядит как код вашего примера: return Directory.GetFiles(".").FirstOrDefault(fileName => fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))?.Select(fileName => new StreamReader(filename)); Обратите внимание на ?.оператор между двумя вызовами LINQ. Также люди могут утверждать, что создание таких объектов не является наиболее подходящим использованием LINQ, но я думаю, что здесь все в порядке. Это не ответ на ваш вопрос, но отвлекает внимание от одной его части.
Panzercrisis

Ответы:


7

Это антипаттерн, так как он принимает вид:

loop over a set of values
   if current value meets a condition
       do something with value
   end
end

и может быть заменен на

do something with value

Классический пример этого кода:

for (var i=0; i < 5; i++)
{
    switch (i)
        case 1:
            doSomethingWith(1);
            break;
        case 2:
            doSomethingWith(2);
            break;
        case 3:
            doSomethingWith(4);
            break;
        case 4:
            doSomethingWith(4);
            break;
    }
}

Когда следующее работает просто отлично:

doSomethingWith(1);
doSomethingWith(2);
doSomethingWith(3);
doSomethingWith(4);

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

Вот почему это анти-паттерн: он берет паттерн "зациклить и проверить" и злоупотребляет им.

По поводу вашего второго вопроса: да. Шаблон try do более устойчив, чем шаблон test to do, в любой ситуации, когда ваш код не является единственным потоком на всем устройстве, который может изменить состояние тестируемого элемента.

Проблема с этим кодом:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
}

является то , что в то время между File.Existsи при StreamReaderпопытке открыть этот файл, другой поток или процесс может удалить файл. Таким образом, вы получите исключение. Поэтому это исключение необходимо защищать с помощью чего-то вроде:

try
{
    return new StreamReader("desktop.ini");
}
catch (File­Not­Found­Exception)
{
    return null; // or whatever
}

@ Флатер поднимает хороший вопрос? Это сам по себе антипаттерн? Используем ли мы исключения в качестве потока управления?

Если код читает что-то вроде:

try
{
    if (!File.Exists("desktop.ini")
    {
        throw new IniFileMissingException();
        return new StreamReader("desktop.ini");
    }
}
catch (IniFileMissingException)
{
    return null;
}

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

Конечно, то, что мы действительно хотим, - это более элегантный способ создания потока. Что-то вроде:

return TryCreateStream("desktop.ini", out var stream) ? stream : null;

И я бы порекомендовал обернуть этот try catchкод в такой служебный метод, если вы часто используете этот код.


1
@ Флатер, я склонен согласиться с тем, что он не идеален (хотя я спорю, что это пример анти-паттерна, о котором говорят разговоры). Код должен показывать свое намерение, а не механику. Поэтому я бы предпочел что-то вроде Scala Try, например return Try(() => new StreamReader("desktop.ini")).OrElse(null);, но C # не поддерживает эту конструкцию (без использования сторонней библиотеки), поэтому мы должны работать с неуклюжей версией.
Дэвид Арно

1
@ Флатер, я полностью согласен, что исключения должны быть исключительными. Но они редко бывают. Например, файл, который не существует, вряд ли является исключительным, но File.Openвыдает его, если не может найти файл. Поскольку у нас уже есть исключительное исключение, перехват его как части потока управления является необходимостью (если только кто-то не хочет просто позволить приложению аварийно завершить работу).
Дэвид Арно

1
@Flater: второй пример кода - это правильный способ открыть файл. Все остальное вводит условия гонки. Даже если вы проверяете существование файлов, между этой проверкой и когда вы фактически открываете ее, что-то может сделать этот файл недоступным для вас, и вызов Open () взорвется. Так что вы должны быть готовы к исключению в любом случае. Так что вы можете просто не беспокоиться о проверке и просто попытаться открыть его. Исключения - это инструменты, которые помогают нам делать вещи, и их использование не должно рассматриваться как религиозная догма.
whatsisname

1
@Flater: любой способ избежать попыток выполнения операций с файлами представляет условия гонки. Файловая система, в которую вы хотите записать, может стать недоступной за мгновение до вызова File.Create, что делает любую проверку недействительной.
whatsisname

1
@Flater « Вы фактически утверждаете, что каждый вызов метода нуждается в типе возврата ... который фактически пытается по-новому изобрести исключения ». Правильный. Хотя это переосмысление не мое. Это использовалось в функциональных языках в течение многих лет. Они, как правило, избегают целых «исключений как проблемы потока управления» (которые вы вводите в заблуждение как антишаблон на одном дыхании, а затем приводите доводы в пользу следующего), используя типы объединения, например, в этом случае мы будем использовать Maybe<Stream>тип который возвращает, nothingесли файл не существует, и поток, если он существует.
Дэвид Арно

1

Вопрос 1: (это для петли антипаттерн)

Да, потому что вам не нужно самостоятельно искать элементы, хранящиеся в запрашиваемой подсистеме.

В абстрактном виде файловая система, как и база данных, способна отвечать на запросы. При взаимодействии с запрашиваемой подсистемой у нас есть фундаментальный выбор: хотим ли мы, чтобы такая подсистема перечисляла нам свое содержимое, чтобы мы выполняли сопоставление вне подсистемы, или использовали собственные возможности запроса подсистемы.

Давайте на мгновение представим, что вы ищете запись в базе данных вместо файла в каталоге в файловой системе. Вы бы предпочли увидеть

SELECT * FROM SomeTable;

и затем в цикле (например, в C #) над возвращенным курсором, ища ID = 100, или позволяя запрашиваемой подсистеме делать все возможное, чтобы найти именно то, что вы ищете вместо этого?

SELECT * FROM SomeTable WHERE ID = 100;

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


Вопрос 2: Чтобы исправить второй пример, вы бы переписали его без оператора if и вместо этого окружили StreamReader блоком try-catch, а если он выдает исключение FileNotFoundException, вы обрабатываете его соответствующим образом в блоке catch?

Да, потому что именно так работает этот конкретный API - это не совсем наш выбор, так как это библиотечная функция. Проверка if перед вызовом не предлагает никакого дополнительного значения: мы все равно должны использовать try / catch, поскольку (1) могут возникнуть другие ошибки, кроме FileNotFound, и (2) условие гонки.


0

Вопрос 1:

Это из-за возврата нового StreamReader (имя файла); внутри цикла? или тот факт, что вам не нужен цикл for в этом случае?

Стример не имеет к этому никакого отношения. Антишаблон возникает из - за четкий конфликт намерений между foreachи if:

Какова цель foreach?

Я предполагаю, что ваш ответ будет что-то вроде: «Я хочу многократно выполнить определенный кусок кода»

Сколько файлов вы ожидаете обработать?

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

Это также подтверждается тем фактом, что вы немедленно возвращаете значение. Вы на самом деле не заботитесь о втором матче, даже если бы он существовал.


Бывают ситуации, когда это не анти-паттерн.

  • Если вы также посмотрите в подкаталогах ( Directory.GetFiles(".", SearchOption.AllDirectories)), то можно найти более одного файла с одинаковым именем файла (включая расширение)
  • Если вы ищете частичные совпадения имен файлов (например, каждый файл, имя которого начинается с "Test_", или каждый "*.zip"файл.

Обратите внимание, что в обоих этих случаях вам потребуется обработать несколько совпадений и, следовательно, не вернуть значение сразу.


Вопрос 2:

Чтобы исправить второй пример, вы бы переписали его без оператора if и вместо этого окружили StreamReader блоком try-catch, и если он выдает исключение FileNotFoundException, вы обрабатываете его в блоке catch соответственно?

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

По этой причине вы не должны удалять if.

Согласно этому ответу на SoftwareEngineering.SE :

Как правило, использование исключений для потока управления является анти-паттерном с заметными ситуациями и специфическими для конкретного языка кашлями.

В качестве краткого изложения, почему, как правило, это анти-шаблон:

  • Исключением являются, по сути, сложные заявления GOTO
  • Программирование с исключениями, следовательно, приводит к более сложному чтению и пониманию кода
  • Большинство языков имеют существующие структуры управления, предназначенные для решения ваших проблем без использования исключений.
  • Аргументы в пользу эффективности, как правило, являются спорными для современных компиляторов, которые стремятся оптимизировать с предположением, что исключения не используются для потока управления.

Прочитайте обсуждение в вики Ward для получения более подробной информации.

Нужно ли вам обернуть это в try / catch, в значительной степени зависит от вашей ситуации:

  • Насколько вероятно, что вы столкнетесь с условиями гонки?
  • Вы действительно в состоянии справиться с этой ситуацией, или вы хотите, чтобы эта проблема всплыла перед пользователем, потому что вы не знаете, как справиться с этим.

Ничто не является вопросом «всегда используй это». Чтобы доказать мою точку зрения:

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

Так почему же мы не все время носим это защитное снаряжение?

Простой ответ заключается в том, что у них есть недостатки:

  • Стоит денег
  • Это делает ваше движение более громоздким
  • Это может быть довольно тепло носить его.

Теперь мы добираемся куда-то: есть за и против . Другими словами, имеет смысл носить это оборудование только в тех случаях, когда профессионал перевешивает минусы.

  • Строители гораздо чаще получают травмы во время работы. Им выгоден защитный шлем.
  • Офисные работники, с другой стороны, имеют гораздо меньше шансов получить травму. Защитные шлемы не стоят того.
  • Член команды спецназа гораздо более вероятен, чем офисный работник.

Стоит ли обернуть звонок в попытку / ловить? Это очень сильно зависит от того, перевешивает ли польза от этого затраты на его реализацию.

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

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

Так что выбор за вами. Есть ли польза от этого? Считаете ли вы, что это улучшает приложение, а не требует затрат на его реализацию?

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

Это очень сильно зависит от окружающего контекста.

  • Если открытию потокового ридера предшествует if(!File.Exists) File.Create(), то отсутствие файла при открытии потокового ридера действительно исключительное .
  • Если имя файла было выбрано из списка существующих файлов, его внезапное отсутствие снова является исключительным .
  • Если вы работаете со строкой, которую вы еще не проверяли в каталоге; тогда отсутствие файла является вполне логичным результатом, и, следовательно, не исключительным .
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.