Елегантні способи впоратися, якщо (якщо ще)


161

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

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • Чи є така назва логіки?
  • Я занадто OCD?

Я відкритий для злих пропозицій щодо коду, хоча б заради цікавості ...


8
@Emmad Kareem: два DefaultActionдзвінки порушують принцип DRY
Abyx

Дякую за Вашу відповідь, але я думаю, що це нормально, за винятком того, що не використовується "try / catch", оскільки можуть виникнути помилки, які не повертають результатів і можуть спричинити відхилення (залежно від мови програмування).
NoChance

20
Я думаю, що головне питання тут полягає в тому, що ви працюєте на неузгоджених рівнях абстракції . Більш високий рівень абстракції: make sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction(). Деталі нітроподібного зерна, що забезпечують наявність даних для DoSomething (), знаходяться на нижчому рівні абстрагування, і тому повинні виконувати інші функції. Ця функція матиме назву на вищому рівні абстракції, а її реалізація буде низькорівневою. Хороші відповіді нижче стосуються цього питання.
Гілад Наор

6
Будь ласка, вкажіть мову. Можливі рішення, стандартні ідіоми та давні культурні норми відрізняються для різних мов і призведуть до різних відповідей на ваш Q.
Caleb

1
Ви можете посилатися на цю книгу "Рефакторинг: вдосконалення дизайну існуючого коду". Є кілька розділів про структуру "if-else", справді корисну практику.
Вакер

Відповіді:


96

Витягніть його для розділення функції (методу) та використання returnоператора:

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();

Або, можливо, краще, розділити отримання вмісту та його обробку:

contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();

Оновлено:

Чому б не винятки, чому OpenFileне викидається виняток IO:
я думаю, що це дійсно загальне питання, а не питання про файл IO. Назви, такі як FileExists, OpenFileможуть бути заплутаними, але якщо замінити їх на Foo, Barтощо, - було б зрозуміліше, що вони DefaultActionможуть називатися так часто, як DoSomethingце може бути не винятково. Про це писав Петер Трьок наприкінці своєї відповіді

Чому існує третій умовний оператор у 2-му варіанті:
Якщо був би тег [C ++], я б написав ifзаяву з декларацією contentsв частині своєї умови:

if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();

Але для інших (С-подібних) мов - if(contents) ...; else ...;це точно те саме, що й вираз із термінальним умовним оператором, але довше. Оскільки основна частина коду була get_contentsфункцією, я просто використовував більш коротку версію (а також contentsтип опущений ). У всякому разі, це поза цим питанням.


93
+1 для кількох повернень - коли методів зроблено досить мало , цей підхід працював найкраще для мене
gnat

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

3
Кілька шляхів повернення можуть мати негативні наслідки для програм C ++, перемагаючи зусилля оптимізатора використовувати RVO (також NRVO, якщо кожен шлях не повертає один і той же об'єкт).
Functastic

Я рекомендую змінити логіку у 2-му рішенні: {if (файл існує) {встановити вміст; if (sometest) {повернути вміст; }} повернути нуль; } Це спрощує потік і зменшує кількість рядків.
Клин

1
Привіт Abyx, я помітив, що ви включили деякі відгуки з коментарів тут: дякую, що зробили це. Я прибрав усе, на що звертався у вашій відповіді та інших відповідях.

56

Якщо мова програмування, якою ви користуєтесь (0) двійкові порівняння короткого замикання (тобто, якщо не викликає, SomeTestякщо FileExistsповертає помилку), та (1) присвоєння повертає значення (результат OpenFileприсвоюється, contentsа потім це значення передається як аргумент до SomeTest), ви можете використовувати щось на кшталт наступного, але все ж радимо вам прокоментувати код, зазначивши, що сингл =навмисний.

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

Залежно від того, наскільки складений результат if, може бути краще мати змінну прапора (яка відокремлює тестування умов успіху / відмови від коду, який обробляє помилку DefaultActionв цьому випадку)


Ось як я це зробив би.
Антоній

13
На ifмою думку, досить брутально ставити так багато коду у заяві.
moteutsch

15
Мені, навпаки, подобається таке твердження "якщо щось існує і відповідає цій умові". +1
Горпік

Я - також! Мені особисто не подобається те, як люди використовують декілька повернень, деякі приміщення не дотримуються. Чому ви не інвертувати це умовний спосіб і виконати свій код , якщо вони будуть виконані?
klaar

"Якщо щось існує і відповідає цій умові", це добре. "якщо щось існує і щось тут дотично пов'язане і відповідає цій умові", OTOH, бентежить. Іншими словами, мені не подобаються побічні ефекти в стані.
Пісквор

26

Більш серйозно, ніж повторення виклику до DefaultAction - це сам стиль, оскільки код написаний неортогонально (див. Цю відповідь для вагомих причин написання ортогонально).

Щоб показати, чому неортогональний код поганий, розглянемо оригінальний приклад, коли введено нову вимогу, що ми не повинні відкривати файл, якщо він зберігається на мережевому диску. Ну, тоді ми могли просто оновити код на наступне:

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Але тоді також виникає вимога, що ми також не повинні відкривати великі файли понад 2Gb. Ну, ми просто оновлюємо ще раз:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Повинно бути дуже зрозуміло, що такий стиль коду буде величезним болем у обслуговуванні.

Серед відповідей, написаних належним чином ортогонально, є другий приклад Абікса та відповідь Яна Гудека , тому я не повторюю цього, просто зазначу, що додавання двох вимог до цих відповідей було б просто

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(або goto notexists;замість цього return null;), не впливаючи на будь-який інший код, крім доданих рядків . Наприклад, ортогональна.

При тестуванні загальним правилом має бути тестування винятків, а не звичайний випадок .


8
+1 для мене. Раннє повернення допомагає уникнути анти-шаблону стрілок. Дивіться codinghorror.com/blog/2006/01/flattening-arrow-code.html та lostechies.com/chrismissal/2009/05/27/… Перш ніж читати про цей шаблон, я завжди підписався на 1 запис / вихід на функцію теорія завдяки тому, що мене навчали 15 років або близько того. Я вважаю, що це просто робить код набагато простішим для читання і, як ви згадуєте, більш ретельним.
Містер Муз

3
@MrMoose: ваша згадка про антидіапазон стрілок відповідає чіткому питанню Бенжола: "Чи існує така назва логіки?" Опублікуйте це як відповідь, і ви отримаєте мій голос.
outis

Це чудова відповідь, дякую. І @MrMoose: "стрілочний анти-шаблон", можливо, відповідає моїй першій кулі, так що так, зробіть це. Не можу пообіцяти, що прийму це, але він заслуговує на голоси!
Benjol

@outis. Дякую. Я додав відповідь. Анти-шаблон стрілки, безумовно, є актуальним на посаді hlovdal, і його охоронні застереження добре працюють над тим, щоб їх обійти. Я не знаю, як ви могли відповісти на цю точку другої кулі. Я не кваліфікований для діагностування цього :)
Містер Муз

4
+1 за "винятки з тесту, не звичайний випадок".
Рой Тінкер

25

Очевидно:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

Ви сказали, що ви відкриті навіть для злих рішень, тому, використовуючи зло, перераховуєте, чи ні?

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

Коли у вас є винятки, їх буде легше читати, особливо якщо ви можете мати OpenFile та DoSomething просто викинути виняток, якщо умови не виконуються, тому явні перевірки вам не потрібні. З іншого боку, на C ++, викид Java і C # виняток - це повільна операція, тому з точки зору продуктивності, goto все ще є кращим.


Примітка про "зло": C ++ FAQ 6.15 визначає "зло" як:

Це означає, що таке і таке - це те, чого слід уникати більшу частину часу, але не те, чого слід уникати весь час. Наприклад, ви в кінцевому підсумку використовуєте ці "злі" речі, коли вони будуть "найменшим злом із злих альтернатив".

І це стосується gotoцього контексту. Структуровані конструкції управління потоком краще більшу частину часу, але коли ви потрапляєте в ситуацію, коли вони накопичують занадто багато власного зла, наприклад, присвоєння в умові, вкладення більш ніж на 3 рівні глибоко, дублювання коду або довгі умови, gotoможе просто закінчитися бути менш злим.


11
Мій курсор наводить курсор на кнопку "Прийняти" ... просто щоб викривити всіх пуристів. Oooohh спокуса: D
Benjol

2
Так Так! Це абсолютно «правильний» спосіб написання коду. Структура коду зараз "Якщо помилка, помилка керування. Нормальна дія. Якщо помилка, помилка керування. Нормальна дія", яка точно така, яка повинна бути. Весь "звичайний" код записується лише з одним рівнем відступу, тоді як весь код, пов'язаний з помилками, має два рівні відступи. Таким чином, звичайний і найважливіший код отримує найвизначніше візуальне місце, і можна дуже швидко і легко читати потік послідовно вниз. Прийміть цю відповідь будь-якими способами.
hlovdal

2
І ще один аспект полягає в тому, що код, написаний таким чином, є ортогональним. Наприклад, два рядки "if (! FileExists (file)) \ n \ tgoto notexists;" зараз ТІЛЬКИ пов'язані з обробкою цього аспекту єдиної помилки (KISS), і найголовніше, що це не стосується жодного з інших рядків . Ця відповідь stackoverflow.com/a/3272062/23118 перераховує кілька вагомих причин для збереження коду ортогональним.
hlovdal

5
Говорячи про злі рішення: Я можу мати ваше рішення без goto:for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
herby

4
@herby: Ваше рішення є більш злим, ніж gotoчерез те, що ви зловживаєте breakтаким чином, що ніхто не очікує, що його зловживають, тому люди, які читають код, матимуть більше проблем із побаченням того, куди приводить перерва, ніж з goto, який прямо говорить. Крім того, ви використовуєте нескінченний цикл, який запуститься лише один раз, що буде досить заплутано. На жаль, do { ... } while(0)це не зовсім читабельно, тому що ви бачите, що це просто кумедний блок, коли ви доходите до кінця, а C не підтримує зривання з інших блоків (на відміну від perl).
Ян Худек

12
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

або зайдіть на зайвого чоловіка та створіть додатковий метод FileExistsAndConditionMet (файл) ...
UncleZeiv

@herby SomeTestможе мати ту саму семантику, що і існування файлу, якщо SomeTestперевіряє тип файлу, наприклад перевіряє, що .gif дійсно GIF-файл.
Абікс

1
Так. Залежить. @Benjol знає краще.
herby

3
... звичайно, я мав на увазі "піти зайвим милом" ... :)
UncleZeiv

2
Тобто з равіолі в кінцівки , навіть я не ходжу (і я буду крайнім в цьому) ... Я думаю , що зараз це добре читається розгляд contents && f(contents). Дві функції для збереження ще однієї ?!
herby

12

Одна можливість:

boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}

Звичайно, це робить код трохи складнішим по-іншому. Тож це багато в чому питання стилю.

Інший підхід використовує винятки, наприклад:

try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}

Це виглядає простіше, однак воно застосовується лише у тому випадку, якщо

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

19
Нуоо ~! Не змінна прапор, це, безумовно, неправильний шлях, оскільки це призводить до складного, важкого для розуміння (де-що-прапор-стає-істинним) і важкого коду рефактора.
Абікс

Не, якщо ви обмежите його якомога більш локальним обсягом. (function () { ... })()у Javascript, { flag = false; ... }у C-подібному тощо
herby

+1 за логіку винятку, яка дуже добре може бути найбільш підходящим рішенням залежно від сценарію.
Стівен Євріс

4
+1 Цей взаємний "Nooooo!" смішно. Я думаю, що змінна статусу та дострокове повернення є розумними в певних випадках. У більш складних підпрограмах я б хотів би змінити статус, тому що, замість додавання складності, те, що він насправді робить, робить логіку явною. Нічого поганого в цьому немає.
grossvogel

1
Це наш бажаний формат, де я працюю. Двома основними корисними варіантами здаються "множинні повернення" та "змінні прапорця". Здається, ні в середньому немає справжньої переваги, але обидва підходять до певних обставин краще, ніж інші. Доводиться йти зі своїм типовим випадком. Просто чергова релігійна війна "Emacs" проти "Vi". :-)
Брайан Ноблеуч

11

Це на більш високому рівні абстракції:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

І це заповнює деталі.

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}

11

Функції повинні виконувати одне. Вони повинні це зробити добре. Вони повинні робити це лише.
- Роберт Мартін в чистому коді

Деякі люди вважають такий підхід трохи екстремальним, але він також дуже чистий. Дозвольте мені проілюструвати на Python:

def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)

Коли він каже, що функції повинні виконувати одне, він означає одне . processFile()вибирає дію на основі результату тесту, і це все, що робить. fileMeetsTest()поєднує всі умови тесту, і це все, що він робить. contentsTest()передає перший рядок firstLineTest(), і це все, що він робить.

Здається, що це багато функцій, але він читається практично як пряма англійська:

Щоб обробити файл, перевірте, чи відповідає він тесту. Якщо так, то зробіть щось. В іншому випадку виконайте дії за замовчуванням. Файл відповідає тесту, якщо він існує, і проходить перевірку вмісту. Щоб перевірити вміст, відкрийте файл і протестуйте перший рядок. Тест для першого рядка ...

Зрозуміло, це трохи зрозуміло, але зауважте, що якщо технічному обслуговувачеві не важливо деталей, він може перестати читати лише після 4-х рядків коду processFile(), і він все ще матиме гарні знання на високому рівні про те, що робить ця функція.


5
+1 Це хороша порада, але те, що становить "одне", залежить від поточного шару абстракції. processFile () - це "одна річ", але дві речі: fileMeetsTest () і doSomething () або defaultAction (). Я побоююся, що аспект "одне" може заплутати початківців, які не апріорі розуміють концепцію.
Калеб

1
Це гарна мета ... Це все, що я маю про це сказати ... ;-)
Брайан Ноблеуч

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

@Caleb, ProcessFile () справді робить одне. Як каже Карл у своєму дописі, він використовує тест, щоб вирішити, яку дію вжити, та відклавши фактичну реалізацію можливостей дії для інших методів. Якби ви додали ще багато альтернативних дій, критерії єдиного призначення цього методу все одно будуть дотримані до тих пір, поки в безпосередньому методі не відбувається введення логіки.
S.Robins

6

Що стосується того, як це називається, він може легко перетворитись у антидіапазон стрілок, оскільки ваш код зростає для задоволення більше вимог (як показано у відповіді, поданій на https://softwareengineering.stackexchange.com/a/122625/33922 ) та потім потрапляє у пастку величезних розділів кодів із вкладеними умовними висловлюваннями, що нагадують стрілку.

Дивіться такі посилання, як;

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/

У цій мережі Google можна дізнатися більше про ці та інші анти-шаблони.

Деякі чудові поради, які Джефф дає у своєму блозі щодо цього, є;

1) Замініть умови охоронними пунктами.

2) Розкласти умовні блоки на окремі функції.

3) Перетворити негативні чеки в позитивні чеки

4) Завжди опортуністично повертайтеся якомога швидше з функції.

Дивіться деякі коментарі до блогу Джеффа щодо пропозицій Стіва МакКоннеллса щодо раннього повернення;

"Використовуйте повернення, коли воно підвищує читабельність. У певних підпрограмах, коли ви дізнаєтесь відповідь, ви хочете негайно повернути її в процедуру виклику. Якщо порядок визначений таким чином, що він не потребує подальшої очистки, як тільки він буде виявляє помилку, не повертаючись відразу, означає, що вам доведеться написати більше коду. "

...

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

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


6

Це відповідає правилам DRY, no-goto та no-multiple-return, на мій погляд, масштабоване та читабельне:

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

1
Однак, відповідність стандартам не обов'язково відповідає доброму коду. Наразі я не визначився з цим фрагментом коду.
Брайан Кноблауш

це просто замінює 2 defaultAction (); з 2 однаковими, якщо умови, і додає змінну прапора, яка imo набагато гірша.
Рятал

3
Перевага використання такої конструкції полягає в тому, що при збільшенні кількості тесту код не починає гніздитися більше ifs всередині інших ifs. Крім того, код для обробки невдалого випадку ( DefaultAction()) є лише в одному місці, і для цілей налагодження код не стрибає навколо допоміжних функцій, а додавання точок прориву до рядків, де successзмінена змінна, може швидко показати, які тести пройшли (вище задіяного точка розриву) та які не перевірені (нижче).
заморожений

1
Так, мені це подобається, але я думаю, що я перейменував successби його ok_so_far:)
Benjol

Це дуже схоже на те, що я роблю, коли (1) процес дуже лінійний, коли все йде правильно, і (2) у протилежному випадку у вас буде антидіаграма. Я, однак, намагаюся уникати додавання додаткової змінної, що зазвичай легко, якщо ви думаєте з точки зору передумов для наступного кроку (що тонко відрізняється від запитання, якщо попередній крок не вдався). Якщо файл існує, відкрийте файл. Якщо файл відкритий, прочитайте вміст. Якщо у мене є вміст, обробляйте їх, інакше виконайте дії за замовчуванням.
Адріан Маккарті

3

Я витягну його окремим методом, а потім:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

що також дозволяє

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

то, можливо, ви зможете видалити DefaultActionдзвінки та залишити виконання DefaultActionдля абонента:

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

Мені також подобається підхід Жанни Піндар .


3

У цьому конкретному випадку відповідь досить проста ...

Існує умова гонки між FileExistsта OpenFile: що відбувається, якщо файл буде видалений?

Єдиний розумний спосіб вирішити цей конкретний випадок - пропустити FileExists:

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

Це акуратно вирішує цю проблему і робить код чистішим.

Взагалі: Спробуйте переосмислити проблему і розробити інше рішення, яке повністю уникне проблеми.


2

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

Таким чином ваш приклад може стати:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

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


2

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

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

Це безпосередньо не стосується питання змішування рівня абстракції, оскільки воно уникає проблеми декількох, недосяжних тестів цілком, хоча усунення тесту існування файлу не сумісне з розділенням рівнів абстракції. Якщо припустити, що недійсні ручки файлу еквівалентні "помилковим", а ручки файлів закриваються, коли вони виходять за межі області:

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}

2

Я згоден із замороженою, однак, для C # все одно, я думав, що це допоможе прослідкувати синтаксис методів TryParse.

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}

1

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

if (!ProcessFile(file)) { 
  DefaultAction(); 
}

Пишуть програмісти Perl і Ruby processFile(file) || defaultAction()

Тепер перейдіть до ProcessFile:

if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;

1

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

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Можливо, вам потрібні додаткові фільтри. Потім зробіть це:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Хоча це може мати сенс також:

function eat(apple:Apple) {
     if (isEdible(apple)) 
         digest(apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

Який найкращий? Це залежить від реальної світової проблеми, з якою ви стикаєтесь.
Але все, що потрібно забрати, це: ти можеш багато зробити зі складом та поліморфізмом.


1

Що не так з очевидним

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

Мені це здається досить стандартним? Для такої великої процедури, коли має відбуватися багато дрібниць, невдача жодної з яких не завадила б останнім. Винятки роблять це трохи чистішим, якщо це варіант.


0

Я розумію, що це старе питання, але я помітив шаблон, про який не згадувалося; головним чином, встановлення змінної, щоб пізніше визначити метод / методи, які ви хочете зателефонувати (за винятком параметра if if else ...).

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

Замість того, щоб замінювати всі згадки методу (і, можливо, відсутні деякі сценарії), всі вони перераховані в кінці блоку if ... else ... і їх простіше читати та змінювати. Я схильний використовувати це, коли, наприклад, може бути викликано кілька методів, але всередині вкладених, якщо ... ще ... метод може бути викликаний у декількох збігах.

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

Це може бути використано, як у прикладі, заданому у запитанні, де ми перевіряємо, чи сталося "DoSomething", а якщо ні, виконуємо дію за замовчуванням. Або у вас може бути стан для кожного методу, який ви хочете зателефонувати, встановити, коли це застосовується, а потім викликати відповідний метод поза межами if ... else ...

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

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}

0

Щоб зменшити вкладені IF:

1 / раннє повернення;

2 / складна експресія (відомо про коротке замикання)

Отже, ваш приклад може бути відновлений так:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();

0

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

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

Якщо ви хочете писати менше рядків або ненавидите нескінченні цикли як я, ви можете змінити тип циклу на "do ... while (0)" і уникнути останнього "break".


0

Як щодо цього рішення:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

Я зробив припущення, що OpenFile повертає вказівник, але це може працювати також із поверненням типу значення, вказавши якесь значення за замовчуванням, яке не можна повернути (коди помилок чи щось подібне).

Звичайно, я не чекаю можливих дій через метод SomeTest на покажчику NULL (але ніколи не знаєш), тому це також може розглядатися як додаткова перевірка покажчика NULL для виклику SomeTest (вмісту).


0

Зрозуміло, що найелегантніше і стисліше рішення - використовувати макрос препроцесора.

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

Що дозволяє писати такий красивий код:

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

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


Для деяких людей, а в деяких мовах, препроцесорні макроси є злими кодами :)
Benjol

@Benjol Ви сказали, що ви відкриті для злих пропозицій, ні? ;)
Пітер Олсон

так, абсолютно, це було просто wrt ваше "уникнути зла" :)
Benjol

4
Це так жахливо, що я просто мусив підтвердити це: D
back2dos

Ширлі, ти несерйозна !!!!!!
Джим у Техасі

-1

Оскільки ви задали цікавість, і ваше запитання не позначене конкретною мовою (навіть незважаючи на те, що ви мали на увазі необхідні мови), можливо, варто додати, що мови, що підтримують ледачу оцінку, дозволяють отримати зовсім інший підхід. У цих мовах вирази оцінюються лише за потреби, тож ви можете визначити "змінні" та використовувати їх лише тоді, коли це має сенс. Наприклад, вигаданою мовою з ледачими let/ inструктурами ви забуваєте про контроль потоку і пишете:

let
  contents = ReadFile(file)
in
  if FileExists(file) && SomeTest(contents) 
    DoSomething(contents)
  else 
    DefaultAction()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.