Как избежать каскадного рефакторинга?


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, затем отменить A и сначала выполнить B.
rwong

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

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

Ответы:


69

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

Затем я начал анализировать, что пошло не так, и разработал лучший план, как выполнить рефакторинг в более мелкие шаги. Поэтому мой совет избегать каскадного рефакторинга: знайте, когда остановиться , не позволяйте вещам выйти из-под вашего контроля!

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

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


53

Является ли это просто симптомом моих предыдущих занятий, которые слишком сильно зависят друг от друга?

Конечно. Одним из изменений, вызывающих множество других изменений, является определение сцепления.

Как мне избежать каскадных рефакторов?

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

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


33
+1 Чем серьезнее требуется рефакторинг, тем шире будет рефакторинг. Это сама природа вещи.
Пол Дрэйпер

4
Однако если вы действительно проводите рефакторинг , другой код не должен беспокоиться об изменениях сразу. (Конечно, вы в конечном итоге захотите очистить другие части ... но это не должно быть необходимо немедленно.) Изменение, которое «каскадно» пронизывает остальную часть приложения, больше, чем рефакторинг - в этот момент это в основном редизайн или переписать.
Цао

+1 Адаптер - это именно тот способ, которым нужно изолировать код, который вы хотите изменить первым.
winkbrace

17

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

Если вы сохраняете каждое индивидуальное изменение минимальным, для рефакторинга действительно не будет возможности сломать вашу сборку надолго. В худшем случае, вероятно, изменение параметров метода в широко используемом интерфейсе, например, для добавления нового параметра. Но вытекающие из этого изменения являются механическими: добавление (и игнорирование) параметра в каждой реализации и добавление значения по умолчанию при каждом вызове. Даже если есть сотни ссылок, такой рефакторинг не должен занимать даже дня.


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

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

5
Тогда способ сделать это - удалить все виды использования удаляемой функции перед рефакторингом, чтобы удалить ее, а не после. Это позволяет вам продолжать сборку кода, пока вы над ним работаете.
Жюль

11
@DeadMG: это звучит странно: вы убираете одну функцию, которая больше не нужна, как вы говорите. Но с другой стороны, вы пишете «проект становится полностью неработоспособным» - это на самом деле звучит, что эта функция абсолютно необходима. Просьба уточнить.
Док Браун

26
@DeadMG В таких случаях вы обычно разрабатываете новую функцию, добавляете тесты, чтобы убедиться, что она работает, переводите существующий код для использования нового интерфейса, а затем удаляете (теперь) лишнюю старую функцию. Таким образом, не должно быть точки, в которой вещи ломаются.
Сапи

12

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

Дизайн желаемого мышления

Цель - отличный дизайн и реализация ОО для новой функции. Избежание рефакторинга также является целью.

Начните с нуля и создайте дизайн для новой функции , которую вы хотели бы иметь. Потратьте время, чтобы сделать это хорошо.

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

  • Рефакторинг достаточен только для создания необходимого шва для внедрения / реализации кода новой функции.
    • Стойкость к рефакторингу не должна приводить в движение новый дизайн.
  • Напишите клиентский класс с API-интерфейсом, который сохраняет новую функцию и существующий код в блаженном неведении друг о друге.
    • Он транслитерирует для получения объектов, данных и результатов туда и обратно. Наименьшее знание принципа будь проклят. Мы не собираемся делать ничего хуже того, что уже делает существующий код.

Эвристика, извлеченные уроки и т. Д.

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

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

«Структура» - это все. Структура - это реализация принципа единой ответственности; дизайн, который облегчает функциональность. Код будет оставаться коротким и простым на протяжении всей иерархии классов. Время на новый дизайн уходит во время тестирования, переделки и предотвращения взлома устаревших джунглей кода.

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

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


9

Из (замечательной) книги « Эффективная работа с устаревшим кодом» Майкла Фезерса :

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

Если позже вы сможете покрыть код вокруг точки, где вы нарушили зависимости, вы можете также вылечить этот шрам.


6

Это звучит так (особенно из обсуждений в комментариях), в которые вы сами включили навязанные им правила, которые означают, что это «незначительное» изменение - такой же объем работы, что и полная переработка программного обеспечения.

Решение должно быть «не делай этого» . Это то, что происходит в реальных проектах. Множество старых API имеют в результате уродливые интерфейсы или заброшенные (всегда нулевые) параметры, или функции с именем DoThisThing2 (), которые делают то же самое, что DoThisThing () с совершенно другим списком параметров. Другие распространенные уловки включают в себя сохранение информации в глобальных или теговых указателях для того, чтобы переправить ее через большой кусок фреймворка. (Например, у меня есть проект, в котором половина аудио-буферов содержит только 4-байтовое магическое значение, потому что это было намного проще, чем изменить способ, которым библиотека вызывала свои аудио-кодеки.)

Трудно дать конкретный совет без конкретного кода.


3

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

Я также рекомендовал бы эти книги.

  • Эффективная работа с устаревшим кодом , перья
  • Рефакторинг , Фаулер
  • Растущее объектно-ориентированное программное обеспечение, руководствуясь тестами , Фриман и Прайс
  • Чистый код , Мартин

3
Ваш вопрос: «Как мне избежать этого [провала] в будущем?» Ответ в том, что, даже если у вас в настоящее время «есть» CI и тесты, вы не применяете их правильно. У меня не было ошибки компиляции, которая длилась более десяти минут в течение многих лет, потому что я рассматриваю компиляцию как «первый модульный тест», и когда она прерывается, я исправляю ее, потому что мне нужно, чтобы тесты проходили как Я работаю дальше над кодом.
Астхаср

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

5
Продолжение рефакторинга, несмотря на неудачную сборку, похоже на мертвую расплату . Это навигационная техника последней инстанции . При рефакторинге возможно, что направление рефакторинга просто неверно, и вы уже видели явный признак этого (момент, когда он прекращает компиляцию, то есть летает без индикаторов воздушной скорости), но вы решили нажать дальше. В конце концов самолет падает с радара. К счастью, нам не нужны «черный ящик» или следователи для рефакторинга: мы всегда можем «восстановить состояние до последнего известного хорошего состояния».
Rwong

4
@DeadMG: вы написали «В моем случае предыдущие вызовы просто больше не имеют смысла», но в вашем вопросе « незначительное изменение интерфейса для его адаптации». Честно говоря, только одно из этих двух предложений может быть правдой. И из вашего описания проблемы кажется довольно ясным, что изменение вашего интерфейса определенно не было незначительным . Вы должны действительно очень серьезно задуматься о том, как сделать ваши изменения более обратно совместимыми. По моему опыту, это всегда возможно, но сначала нужно придумать хороший план.
Док Браун

3
@DeadMG В этом случае я думаю, что то, что вы делаете, не может быть разумно названо рефакторингом, основной смысл которого заключается в применении изменений в дизайне в виде серии очень простых шагов.
Жюль

3

Является ли это просто симптомом моих предыдущих занятий, которые слишком сильно зависят друг от друга?

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

Как я могу избежать такого рода каскадных рефакторингов в будущем?

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

Этот метод называется «Метод Микадо», и он работает так:

  1. запишите цель, которую вы хотите достичь, на листе бумаги

  2. внесите самое простое изменение, которое приведет вас в этом направлении.

  3. проверьте, работает ли он, используя компилятор и ваш набор тестов. Если это продолжается, перейдите к шагу 7. в противном случае перейдите к шагу 4.

  4. на своей бумаге отметьте вещи, которые необходимо изменить, чтобы ваши текущие изменения работали. Нарисуйте стрелки от вашего текущего задания к новым.

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

  6. выберите одну из задач, в которой нет исходящих ошибок (нет известных зависимостей) и вернитесь к 2.

  7. зафиксируйте изменения, вычеркните задачу на бумаге, выберите задачу без исходящих ошибок (без известных зависимостей) и вернитесь к 2.

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


2

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

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


2

... Я переработал проект, чтобы добавить функцию.

Как сказал @Jules, рефакторинг и добавление функций - это две разные вещи.

  • Рефакторинг - это изменение структуры программы без изменения ее поведения.
  • Добавление функции, с другой стороны, увеличивает ее поведение.

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

Мне нужно было сделать небольшое изменение интерфейса, чтобы приспособить его

Вот где все становится грязно. Интерфейсы предназначены как границы, чтобы изолировать реализацию от того, как она используется. Как только вы коснетесь интерфейсов, все с обеих сторон (реализация или использование) также должно быть изменено. Это может распространиться так далеко, как вы это испытали.

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

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


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

... на самом деле, лучший способ избежать каскадных модификаций кода - это именно хорошие интерфейсы. ;)


-1

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


1
кажется, это не дает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 9 ответах
комнат

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