"Як погано" - неспоріднений код у блоці "пробую-нарешті"?


11

Це пов’язано із запитанням: Чи використання остаточного підрозділу для роботи після повернення поганого стилю / небезпечно?

У посиланні Q, нарешті, код пов'язаний із використовуваною структурою та необхідністю попереднього вибору. Моє запитання дещо інше, і я вважаю, що він є широким для широкої аудиторії. Мій конкретний приклад - додаток для формату C #, але це також стосується використання C ++ / Java.

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

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

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

Приблизно:

    methodName (...)
    {
        спробуйте
        {
            // Багато коду для методу ...
            // код, який може кинути ...
            // Більше коду для методу та повернення ...
        }
        зловити (щось)
        {// обробляти виняток}
        нарешті
        {
            // деяка прибирання через виняток, закриття речей
            // більше коду для створених матеріалів (ігноруючи, що будь-які винятки могли кинути) ...
            // можливо створити ще якісь об’єкти
        }
    }

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

Чи виправданий поганий стиль внесення змін? Хтось сильно згорів від подібної ситуації? Чи хотіли б ви поділитися деталями цього поганого досвіду? Залиште це, тому що я надмірно реагую, і це не так погано в стилі? Отримайте переваги в обслуговуванні прибирання речей?


Тег "C ++" сюди не належить, оскільки C ++ не має (і не потребує) finally. Усі корисні користі охоплені RAII / RRID / SBRM (залежно від того, що вам подобається).
Девід Торнлі

2
@DavidThornley: Окрім прикладу, де використовується ключове слово "нарешті", решта питання ідеально відповідає C ++. Я розробник C ++, і це ключове слово не дуже мене бентежило. І враховуючи, що я маю подібні розмови зі своєю власною командою на регулярній основі, це питання дуже стосується того, що ми робимо з C ++
DXM

@DXM: У мене виникають проблеми з візуалізацією цього (і я точно знаю, що finallyробиться в C #). Що таке еквівалент С ++? Те, про що я думаю, - це код після catch, і це застосовується для коду після C # finally.
Девід Торнлі

@DavidThornley: код, нарешті, це код, який виконується незалежно від того .
orlp

1
@nightcracker, це не зовсім правильно. Він не буде виконаний, якщо ви зробите це Environment.FailFast(); він може не бути виконаний, якщо у вас є виняткове вилучення. І це стає ще складніше, якщо у вас є ітераторний блок, finallyякий ви повторюєте вручну.
svick

Відповіді:


10

Я переживав дуже подібну ситуацію, коли мені довелося зіткнутися зі страшним спадковим кодом Windows Forms, написаним розробниками, який явно не знав, що вони роблять.

Перш за все, ти не надто сильний. Це поганий код. Як ви сказали, блок ловлі повинен стосуватися переривання вагітності та підготовки до зупинки. Не час створювати об’єкти (спеціально Панелі). Я навіть не можу почати пояснювати, чому це погано.

Це сумно...

Перша моя порада: якщо вона не зламана, не чіпайте її!

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

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

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

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

Моя третя порада: ви згоряєте, якщо порушите код, не має значення, винна ви чи ні.

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

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

http://www.joelonsoftware.com/articles/fog0000000069.html

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

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

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

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

Мій підхід полягав у тому, щоб переписати його по частинах. Спочатку я замінив увесь інтерфейс (Windows Forms), потім почав замінювати рівень зв'язку (виклики веб-служби) і останній раз замінив всю реалізацію Сервера (це був товстий додаток клієнт / сервер).

Через пару років і ця програма перетворилася на прекрасний ключовий інструмент, який використовує вся компанія. Я на 100% впевнений, що це ніколи не було б можливо, якби я не переписав всю цю справу.

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


1
+1. Але останнє речення стосується серійного вбивці. Це справді потрібно?
MarkJ

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

@RickyClarkson Я згоден. Це дуже важко знати, коли ви перестараєтеся (рефакторинг насправді не потрібен) і коли ви дійсно шкодите програмі, додаючи зміни.
Олексій

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

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

2

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

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

що дасть вам цінні підказки, як зробити код більш перевіреним, додати тести одиниць та уникнути порушення коду при додаванні нових функцій.

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


2

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

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

проти

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

У другому коді ви обробляєте винятки, як коди повернення. Концептуально це ідентично:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

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

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

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

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

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

Повертаючись до початкової мети винятків: пам’ятайте, що вони були створені, оскільки часто код не може легко впоратися з помилками прямо тоді, коли вони насправді трапляються. Суть винятків полягає в тому, що ви можете вирішувати помилки значно пізніше в коді або додатково збільшувати стек. Люди забувають, що і в багатьох випадках їх ловлять локально, коли їм краще просто документувати, що розглянутий метод може викинути цей виняток. У світі занадто багато коду, який виглядає приблизно так:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

Тут ви просто додаєте шари, а шари додають плутанину. Просто зробіть це:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

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

Винос: блок проб повинен містити набір коду, який повинен бути перерваний, якщо в будь-якому місці блоку виникає помилка. Чи не має значення цей набір коду один рядок або тисяча рядків. (Хоча якщо у вас є метод тисячі рядків, у вас є інші проблеми.)


Стівен - дякую за добре викладену відповідь, і я повністю згоден з вашими думками. У мене немає проблеми з розумно пов'язаним або послідовним кодом типу, що живе в одному блоці спробу. Якби я міг опублікувати фактичний фрагмент коду, він показав би 5 - 10 абсолютно не пов'язаних дзвінків до першого дзвінка, що передається. Один конкретний блок, нарешті, був набагато гіршим - кілька нових потоків викручувались та були викликані додаткові процедури, які не є чистими. І з цього приводу всі дзвінки в останньому блоці не пов'язані ні з чим, що могло б кинути.

Однією з головних проблем такого підходу є те, що він не розрізняє випадки, коли method0можна очікувати, що викид може бути кинутий, і той, де його немає. Наприклад, припустимо, що method1іноді може бути викинуто InvalidArgumentException, але якщо цього не відбудеться, він переведе об'єкт у тимчасово недійсний стан, який буде виправлений через method2, який, як очікується, завжди поверне об'єкт у дійсний стан. Якщо method2 закидає InvalidArgumentException, код, який очікує, що випаде такий виняток method1, зловить його, хоча ...
supercat

... стан системи буде відповідати очікуванням коду. Якщо виняток, викинутий з method1, слід обробляти інакше, ніж викинутий методом2, як це можна зробити розумно, не обертаючи їх?
supercat

В ідеалі ваші винятки досить детальні, що це рідкісна ситуація.
Gort the Robot
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.