Як я можу уникати каскадних рефактори?


52

У мене є проект. У цьому проекті я хотів переробити його, щоб додати функцію, і я відновив проект, щоб додати функцію.

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

Як я можу уникнути подібного роду каскадних реконструкцій у майбутньому? Це просто симптом моїх попередніх занять, що занадто сильно залежать один від одного?

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

Більша редакція, яку я обіцяв п'ять днів тому:

Перш ніж я розпочав цей рефактор, у мене була система, де я мав інтерфейс, але в реалізації я просто dynamic_castчерез всі можливі реалізації, які я постачав. Це, очевидно, означало, що ви не можете просто успадкувати інтерфейс, з одного боку, по-друге, що ніхто не зможе отримати доступ до реалізації, щоб реалізувати цей інтерфейс. Тож я вирішив, що хочу виправити цю проблему та відкрити інтерфейс для громадського споживання, щоб хто-небудь міг її реалізувати і щоб реалізація інтерфейсу була необхідною для всього договору - очевидно, вдосконалення.

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

for(auto&& a : as) {
     f(a);
}

Однак, щоб отримати цей контекст, мені потрібно було змінити його на щось подібне

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

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

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


67
Коли ви говорите, що "переробили проект, щоб додати функцію", що саме ви маєте на увазі? Рефакторинг не змінює поведінку програм за визначенням, що робить це твердження заплутаним.
Жуль

5
@Jules: Особливо кажучи, функція полягала в тому, щоб дозволити іншим розробникам додавати певний тип розширення, тому функцією став рефактор, який зробив структуру класу більш відкритою.
DeadMG

5
Я думав, що це обговорюється в кожній книзі та статті, в яких йдеться про рефакторинг? На допомогу приходить джерело контроль; якщо ви виявите, що це зробити крок А, вам слід спершу зробити крок В, а потім скрапте A і зробіть B спочатку.
rwong

4
@DeadMG: це книга, яку я спочатку хотів процитувати у своєму першому коментарі: "Гра" палички для збирання "- це хороша метафора методу Мікадо. Ви усуваєте" технічну заборгованість "- проблеми зі спадщиною, закладені майже в кожному програмному забезпеченні система - дотримуючись набору простих у виконанні правил. Ви обережно витягуєте кожну переплетену залежність, поки не викриєте центральну проблему, не згортаючи проект. "
rwong

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

Відповіді:


69

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

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

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

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


53

Це просто симптом моїх попередніх занять, що занадто сильно залежать один від одного?

Звичайно. Одна зміна, що спричиняє безліч інших змін, є значною мірою визначенням зв'язку.

Як уникнути каскадних рефакторів?

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

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


33
+1 Чим сильніше потрібно рефакторинг, тим ширше буде досягнуто рефакторинг. Це сама природа речі.
Пол Дрейпер

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

+1 Адаптер - це саме спосіб ізолювати код, який потрібно змінити.
winkbrace

17

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

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


4
Я не бачу, як може виникнути така ситуація. Для будь-якого розумного рефакторингу інтерфейсу методу повинен бути легко визначений новий набір параметрів, який може бути переданий, що призведе до того, що поведінка виклику буде такою ж, як і до зміни.
Жуль

3
Я ніколи не був у ситуації, коли хотів виконати таке рефакторинг, але мушу сказати, що це звучить для мене досить незвично. Ви хочете сказати, що ви видалили функціонал з інтерфейсу? Якщо так, куди це пішло? В інший інтерфейс? Або десь ще?
Жуль

5
Тоді спосіб це зробити - це видалити всі звичаї функції, яку потрібно видалити, перш ніж рефакторинг для її видалення, а не після цього. Це дозволяє зберегти побудову коду під час роботи над ним.
Жуль

11
@DeadMG: це звучить дивно: ви видаляєте одну функцію, яка більше не потрібна, як ви говорите. Але з іншого боку, ви пишете "проект стає абсолютно нефункціональним" - це насправді звучить, що функція абсолютно необхідна. Будь ласка, поясніть.
Док Браун

26
@DeadMG У таких випадках ви зазвичай розробляєте нову функцію, додайте тести для забезпечення її роботи, перехід існуючого коду на новий інтерфейс, а потім видалення (тепер) зайвої старої функції. Таким чином, не повинно бути пункту, коли речі ламаються.
сапі

12

Як я можу уникнути такого каскадного рефактора в майбутньому?

Бажаний мислений дизайн

Мета - відмінна розробка та реалізація OO для нової функції. Уникнути рефакторингу також є метою.

Почніть з нуля та створіть дизайн нової функції , яку б ви хотіли. Знайдіть час, щоб зробити це добре.

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

  • Рефактор достатньо лише зробити необхідний шов для введення / реалізації коду нової функції.
    • Стійкість до рефакторингу не повинна спричиняти нову конструкцію.
  • Напишіть клас клієнта, що відповідає API, який утримує нову функцію та існуючий кодек блаженно ігноруючи один одного.
    • Він транслітерується для отримання об'єктів, даних та результатів назад та назад. Принцип найменших знань проклятий. Ми не збираємось робити щось гірше, ніж те, що вже існує.

Евристика, засвоєні уроки тощо.

Рефакторинг був таким же простим, як додавання параметру за замовчуванням до існуючого виклику методу; або один виклик методу статичного класу.

Методи розширення для існуючих класів можуть допомогти зберегти якість нового дизайну з абсолютним мінімальним ризиком.

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

Заняття мислячим бажанням зосереджуються на заданні. Як правило, забудьте про розширення існуючого класу - ви просто знову нагнітаєте каскад рефактора і вам доводиться мати справу з "важчим" класом.

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


9

З (чудової) книги « Ефективна робота зі спадщинним кодексом» Майкла Пера :

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

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


6

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

Рішення повинно бути "не роби цього" . Це те, що відбувається в реальних проектах. У багатьох старих API є некрасиві інтерфейси або покинуті (завжди нульові) параметри в результаті, або функції з назвою DoThisThing2 (), що робить те саме, що DoThisThing () із зовсім іншим списком параметрів. Інші поширені хитрощі включають в себе приховування інформації у глобальних або позначених покажчиках, щоб переправити її через великий шматок фреймворку. (Наприклад, у мене є проект, де половина аудіобуферів містить лише 4-байтне магічне значення, оскільки це було набагато простіше, ніж змінити те, як бібліотека викликала свої аудіокодеки.)

Важко дати конкретні поради без конкретного коду.


3

Автоматизовані тести. Вам не потрібно бути ревнивим TDD, а також не потрібно 100% покриття, але автоматизовані тести - це те, що дозволяє впевнено вносити зміни. Крім того, це здається, що у вас є конструкція з дуже високою муфтою; Ви повинні прочитати про принципи SOLID, які сформульовані спеціально для вирішення подібних проблем у розробці програмного забезпечення.

Я б також рекомендував ці книги.

  • Ефективна робота зі спадковим кодом , пір'я
  • Рефакторинг , Фаулер
  • Вирощування об’єктно-орієнтованого програмного забезпечення, керуючись тестами , Freeman та Pryce
  • Чистий кодекс , Мартіне

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

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

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

4
@DeadMG: Ви написали "У моєму випадку попередні дзвінки просто не мають сенсу більше", але у вашому запитанні " незначна зміна інтерфейсу для його пристосування". Чесно кажучи, лише одне з цих двох речень може бути правдивим. І з опису вашої проблеми видається досить зрозумілим, що зміна вашого інтерфейсу, безумовно, не була другорядною . Вам справді слід по-справжньому думати більше про те, як зробити зміни більш сумісними. На мій досвід, це завжди можливо, але ви повинні спершу придумати хороший план.
Док Браун

3
@DeadMG У цьому випадку я думаю, що те, що ти робиш, неможливо назвати рефакторингом, основний пункт якого полягає в застосуванні змін дизайну у вигляді серії дуже простих кроків.
Жуль

3

Це просто симптом моїх попередніх занять, що занадто сильно залежать один від одного?

Найімовірніше, так. Хоча ви можете отримати подібні ефекти за допомогою досить приємної та чистої бази кодів, коли вимоги досить зміниться

Як я можу уникнути подібного роду каскадних реконструкцій у майбутньому?

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

Цей метод названий "метод Mikado", і він працює так:

  1. запишіть на аркуші паперу мету, яку ви хочете досягти

  2. зробити найпростішу зміну, яка переведе вас у той бік.

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

  4. на папері відзначте речі, які потрібно змінити, щоб ваша поточна зміна працювала. Намалюйте стрілки, починаючи з поточного завдання, до нових.

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

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

  7. здійснити зміну, перекреслити завдання на папері, вибрати завдання, яке не має вихідних помилок (відсутні відомі залежності) і повернутися до 2.

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


2

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

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


2

... Я відновив проект, щоб додати функцію.

Як сказано @Jules, Рефакторинг та додавання функцій - це дві дуже різні речі.

  • Рефакторинг - це зміна структури програми без зміни її поведінки.
  • Додавання функції, з іншого боку, посилює її поведінку.

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

Мені потрібно було внести незначну зміну інтерфейсу, щоб пристосувати його

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

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

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


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

... насправді найкращим способом уникнути каскадних модифікацій коду є саме хороші інтерфейси. ;)


-1

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


1
це, здається, не пропонує нічого суттєвого щодо питань, викладених та пояснених у попередніх 9 відповідях
gnat

@gnat: Можливо, ні, але спростили відповіді.
Тарік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.