За-якщо антипатерн


9

Я читав у цій публікації в блозі про анти-візерунок for-if, і не зовсім впевнений, що розумію, чому це анти-шаблон.

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блок-пробний блок, і якщо він кидає, 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, то зупиніться і подумайте, що ви робите. Ви надмірно ускладнюєте речі і чи зможете весь цикл і тест замінити простою лінією "просто зроби це". Однак іноді вам здасться, що вам потрібно зробити цей цикл (більше одного елемента може відповідати одній умові, наприклад), і в цьому випадку візерунок чудово.

Ось чому це анти-візерунок: він бере "цикл і тест" шаблон і зловживає ним.

Щодо вашого другого питання: так. Шаблон "спробуйте зробити" є більш надійним, ніж шаблон "тестувати, тоді робіть" у будь-яких ситуаціях, коли ваш код не є єдиним потоком на всьому пристрої, який може змінити стан тестуваного елемента.

Проблема з цим кодом:

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
}

@Flater піднімає хороший бал? Це сам антипатерн? Чи використовуємо винятки як контрольний потік?

Якщо код читає щось на кшталт:

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
@Flater, я схильний погоджуватися, що це не ідеально (хоча я заперечую, що це приклад анти-шаблону, про який йдеться у відповіді). Код повинен показувати його наміри, а не механіку. Тому я віддаю перевагу щось на зразок Scala Try, наприклад return Try(() => new StreamReader("desktop.ini")).OrElse(null);, але C # не підтримує цю конструкцію (без використання сторонніх бібліотек), тому нам доводиться працювати з незграбною версією.
Девід Арно

1
@Flater, я повністю згоден, що винятки повинні бути винятковими. Але вони рідко бувають. Наприклад, файл, який не існує, навряд чи є винятковим, але він 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 блоком спробу лову, і якщо він кидає FileNotFoundException, ви відповідно обробляєте його в блоці захоплення?

Так, тому що саме такий API працює - це насправді не наш вибір, оскільки це функція бібліотеки. Якщо перевірка перед викликом не надає додаткового значення: ми все одно повинні використовувати спробувати / ловити, оскільки (1) можуть виникнути інші помилки, крім FileNotFound, та (2) умова перегонів.


0

Питання 1:

Це через повернення нового StreamReader (ім'я файлу); всередині циклу for? чи той факт, що вам не потрібна петля в цьому випадку?

Потоковий читач не має нічого спільного. Анти-модель з'являється через явний конфлікт намірів між foreachта if:

Яка мета роботи foreach?

Я припускаю, що ваша відповідь буде чимось на кшталт: "Я хочу неодноразово виконувати певний фрагмент коду"

Скільки файлів ви очікуєте обробити?

Оскільки у певній папці ви можете мати лише одне конкретне ім’я файлу (включаючи розширення), це доводить, що ваш код призначений для пошуку одного застосовного файлу.

Це також підтверджується тим, що ви повертаєте значення негайно. Вас насправді не хвилює другий матч, навіть якщо він би існував.


Є ситуації, коли це не є антидіаграмою.

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

Зауважте, що обидва ці випадки вимагають від вас фактичної обробки декількох збігів, і тому не повертаєте значення одразу.


Питання 2:

Щоб виправити другий приклад, чи можете ви повторно записати його без оператора if, а замість цього оточити StreamReader блоком спробу-лову, і якщо він кидає FileNotFoundException, ви відповідно обробляєте його в блоці захоплення?

Винятки дорогі. Вони ніколи не повинні використовуватися замість правильної логіки потоку. Винятки становлять, як їх назва говорить про виняткові обставини.

З цієї причини не слід видаляти if.

Відповідно до цієї відповіді на SoftwareEngineering.SE :

Як правило, використання винятків для контрольного потоку є анти-схемою, з помітними ситуаціями та мовою, що стосуються винятків кашлю від кашлю.

Як короткий підсумок, чому, як правило, це анти-шаблон:

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

Прочитайте дискусію на Вікі Уорда, щоб отримати більш детальну інформацію.

Від того, чи потрібно вам це робити, намагайтеся впіймати, дуже залежить від вашої ситуації:

  • Наскільки ймовірно, що ви збираєтеся зіткнутися з умовою гонки?
  • Чи справді ви в змозі впоратися з цією ситуацією, чи ви хочете, щоб ця проблема передавалася користувачеві, оскільки ви не знаєте, як з цим впоратися.

Ніколи не є питанням "завжди користуйся цим". Щоб довести свою думку:

Окремі дослідження довели, що ви рідше травмуєтесь, якщо одягнете захисний шолом, захисні окуляри та бронежилети.

То чому ми всі не носимо це обладнання безпеки?

Проста відповідь полягає в тому, що є їх недоліки:

  • Це коштує грошей
  • Це робить ваш рух більш громіздким
  • Його можна носити досить тепло.

Зараз ми кудись дістаємося: є плюси і мінуси . Іншими словами, носити це обладнання має сенс лише у випадках, коли професіонал переважає проти мінусів.

  • Будівельні працівники набагато частіше зазнають травм під час роботи. Їм користується захисний шолом.
  • З іншого боку, офісні працівники мають набагато менший шанс отримати травму. Шоломи безпеки не варто.
  • Член команди SWAT набагато більше шансів отримати постріл, порівняно з офісним працівником.

Чи слід завершити виклик у спробі / ловінні? Це дуже залежить від того, чи переваги від цього перевищують витрати на його реалізацію.

Зауважте, що інші можуть стверджувати, що для її завершення потрібно лише кілька натискань клавіш, тому це очевидно слід зробити. Але це ще не весь аргумент:

  • Вам потрібно вирішити, що робити, як тільки ви зловите виняток.
  • Якщо по всій кодовій базі багато різних дзвінків до різних файлів, вирішення обгортати один із них, як правило, означає, що вам доведеться завершити всі ці випадки. Це може мати неабиякий вплив на кількість зусиль, необхідних для його здійснення.
  • Цілком можливо, що ви навмисно хочете не обробляти виняток.
    • Зауважте, що ваша програма повинна буде обробляти виняток в якийсь момент, але це не обов'язково одразу після того, як виняток було порушено.

Тож вибір за вами. Чи є користь від цього? Ви вважаєте, що це покращує додаток, а не витрачає зусилля на його реалізацію?

Оновлення - від коментаря, який я написав до іншої відповіді, оскільки я вважаю, що це також є важливим для вас:

Це дуже залежить від навколишнього контексту.

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