Что делать с исходным файлом C ++ из 11000 строк?


229

Таким образом, в нашем проекте есть огромный исходный файл mainmodule.cpp (это 11000 строк?), И каждый раз, когда мне приходится его трогать, я съеживаюсь.

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

Этот файл используется и активно изменяется в нескольких (> 10) версиях нашего продукта, поэтому его очень сложно реорганизовать. Если бы я «просто» разделил его, скажем для начала, на 3 файла, то объединение изменений из версий обслуживания станет кошмаром. А также, если вы разделите файл с такой длинной и богатой историей, отслеживать и проверять старые изменения в SCCистории внезапно становится намного сложнее.

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

Что бы вы сделали в такой ситуации? Любые идеи о том, как перенести новые функции в отдельный исходный файл, не портя SCCрабочий процесс?

(Примечание по инструментам: мы используем C ++ с Visual Studio; мы используем AccuRevкак, SCCно я думаю, что тип SCCздесь не имеет значения; мы используем Araxis Mergeдля фактического сравнения и объединения файлов)


15
@BoltClock: На самом деле Vim откроет его довольно быстро.
2010 года

58
69305 строк и подсчет. Файл в нашем приложении, в который мой коллега выгружает большую часть своего кода. Не могу удержаться, разместив это здесь. У меня нет никого в моей компании, чтобы сообщить об этом.
Агнель Курьян

204
Я не понимаю Как комментарий «бросить эту работу» может получить так много голосов? Некоторые люди, кажется, живут в сказочной стране, где все проекты написаны с нуля и / или используют 100% Agile, TDD, ... (поместите любое из своих умных слов здесь).
Стефан

39
@Stefan: Столкнувшись с подобной кодовой базой, я сделал именно это. Мне не хотелось тратить 95% своего времени на работу с 10-летней базой кода, и 5% фактически писать код. На самом деле было невозможно протестировать некоторые аспекты системы (и я не имею в виду модульное тестирование, я имею в виду на самом деле запустить код, чтобы увидеть, работает ли он). У меня не было 6-месячного пробного периода, я устал бороться, проигрывать битвы и писать код, который не мог стоять в стороне.
Binary Worrier

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

Ответы:


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

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

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

Это оставляет вас с ядром плохо управляемого кода - он нужен везде, но он отличается в каждой ветви (и / или постоянно меняется, так что некоторые ветви работают позади других), и все же он находится в одном файле, который вы безуспешно пытается слиться между ветками. Перестань. Разветвите файл навсегда , возможно, переименовав его в каждой ветви. Это больше не «главное», это «главное для конфигурации X». Итак, вы теряете возможность применять одно и то же изменение к нескольким ветвям путем слияния, но это в любом случае ядро ​​кода, где слияние не работает очень хорошо. Если вам все равно придется вручную управлять слияниями, чтобы справиться с конфликтами, тогда не составит труда применить их независимо друг от друга в каждой ветви.

Я думаю, вы не правы, когда говорите, что тип SCC не имеет значения, потому что, например, возможности git по слиянию, вероятно, лучше, чем используемый вами инструмент слияния. Таким образом, основная проблема, «объединение затруднено», возникает в разное время для разных SCC. Однако вы вряд ли сможете изменить SCC, поэтому проблема, вероятно, не имеет значения.


Что касается слияния: я посмотрел на GIT, посмотрел на SVN, посмотрел на Perforce и позвольте мне сказать, что ничто из того, что я видел, нигде не сравнится с AccuRev + Araxis в том, что мы делаем. :-) (Хотя GIT может сделать это [ stackoverflow.com/questions/1728922/… ], а AccuRev не может - каждый должен сам решить, является ли это частью слияния или анализа истории.)
Мартин Ба

Достаточно справедливо - возможно, у вас уже есть лучший доступный инструмент. Способность Git объединять изменения, которые произошли в файле A в ветви X, в файл B в ветви Y, должна упростить разбиение разветвленных файлов, но, вероятно, система, которую вы используете, имеет те преимущества, которые вам нравятся. В любом случае, я не предлагаю вам переключиться на git, просто говорю, что SCC действительно имеет значение, но даже в этом случае я согласен с вами, что это можно сбрасывать со счетов :-)
Steve Jessop

129

Слияние не будет таким уж страшным кошмаром, как в будущем, когда вы получите 30000 LOC-файлов. Так:

  1. Прекратите добавлять больше кода в этот файл.
  2. Раздели это.

Если вы не можете просто остановить кодирование в процессе рефакторинга, вы можете оставить этот большой файл , как это на некоторое время , по крайней мере , не добавляя больше коды к нему: так как он содержит один «основной класс» вы могли бы наследовать от него и сохранить унаследованный класс ( es) с перегруженными функциями в нескольких новых маленьких и хорошо разработанных файлах.


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

3
С 10 версиями обслуживания и многими активными разработчиками маловероятно, что файл может быть заморожен на достаточно долгое время.
Коби

9
@Martin, у вас есть пара шаблонов GOF, которые бы справились с задачей, один Facade, который отображает функции mainmodule.cpp, в качестве альтернативы (я рекомендовал ниже) создать набор классов Command , каждый из которых отображается на функцию / особенность mainmodule.app. (Я подробно
остановился

2
Да, полностью согласен, в какой-то момент вы должны прекратить добавлять к нему код, или в конечном итоге он станет 30k, 40k, 50k, основной модуль kaboom просто сломался. :-)
Крис

67

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

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

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

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

В любом случае, это звучит как крупный проект, так что удачи :)


18
Он пахнет очень сильно: он пахнет, как анти-шаблон Blob в доме ... en.wikipedia.org/wiki/God_object . Его любимое блюдо - это спагетти-код: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan

@jdehaan: я пытался быть дипломатичным об этом :)
Брайан Расмуссен

+1 От меня тоже, я не смею даже касаться сложного кода, который я написал без тестов.
Дэнни Томас

49

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

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

Как только это будет сделано, вы получите возможность добавлять новые функции в новые классы.

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

Я знаю, что это не идеально, хотя я надеюсь, что это может помочь, и процесс должен быть адаптирован к вашим потребностям!

Дополнительная информация

Обратите внимание, что Git - это SCC, который может следовать фрагментам кода из одного файла в другой. Я слышал хорошие новости об этом, так что это может помочь, когда вы постепенно продвигаетесь в своей работе.

Git построен вокруг понятия BLOB-объектов, которые, если я правильно понимаю, представляют собой фрагменты файлов кода. Переместите эти части в разные файлы, и Git найдет их, даже если вы измените их. Помимо видео Линуса Торвальдса, упомянутого в комментариях ниже, я не смог найти что-то ясное по этому поводу.


Ссылка на то, как GIT делает это / как вы делаете это с GIT, была бы очень кстати.
Мартин Ба

@Martin Git делает это автоматически.
Матфея

4
@Martin: Git делает это автоматически - потому что он не отслеживает файлы, он отслеживает контент. На самом деле в git сложнее просто «получить историю одного файла».
Арафангион

1
@Martin youtube.com/watch?v=4XpnKHJAok8 - это разговор, в котором Торвальдс рассказывает о git. Он упоминает об этом позже в разговоре.
Матфея

6
@Martin, посмотрите на этот вопрос: stackoverflow.com/questions/1728922/…
Бенджол

30

Конфуций говорит: «Первый шаг к выбору из ямы - это прекратить рыть яму».


25

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

Вы понимаете, что наличие огромного файла - это проблема, но еще больше проблем - это десять версий, которые вы должны поддерживать "текущими". Это многократное обслуживание. SCC может сделать это проще, но не может сделать это правильно.

Перед тем, как попытаться разбить файл на части, вам нужно синхронизировать десять ветвей друг с другом, чтобы вы могли видеть и формировать весь код одновременно. Вы можете делать это по одной ветке за раз, проверяя обе ветки на один и тот же файл основного кода. Для реализации пользовательского поведения вы можете использовать #ifdef и друзей, но лучше использовать обычные if / else против определенных констант. Таким образом, ваш компилятор проверит все типы и, скорее всего, все равно удалит «мертвый» объектный код. (Вы можете отключить предупреждение о мертвом коде.)

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

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

В скором времени файл будет расти. Хорошо. То, что вы делаете, это объединение вещей, которые должны быть вместе. После этого вы начнете видеть области, которые явно одинаковы, независимо от версии; они могут быть оставлены в покое или переделаны по желанию. Другие области будут четко различаться в зависимости от версии. У вас есть несколько вариантов в этом случае. Один из методов - делегировать различия объектам стратегии для каждой версии. Другой способ - извлечь версии клиента из общего абстрактного класса. Но ни одна из этих трансформаций невозможна, если у вас есть десять «советов» по ​​развитию в разных отраслях.


2
Я согласен с тем, что цель должна состоять в том, чтобы иметь одну версию программного обеспечения, но не лучше ли использовать конфигурационные файлы (время выполнения), а не компилировать время компиляции
Эсбен Сков Педерсен,

Или даже "классы конфигурации" для сборки каждого клиента.
тк.

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

22

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

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

Я думаю, что у вас есть только эти варианты:

  1. Не мигрируй и оставайся с тем, что имеешь. Возможно, бросите работу и начните работать над серьезным программным обеспечением с хорошим дизайном. Экстремальное программирование не всегда является лучшим решением, если вы работаете над долгосрочным проектом с достаточным количеством средств, чтобы пережить один или два краха.

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

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


19

Именно эта проблема решается в одной из глав книги «Эффективная работа с устаревшим кодом» ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).


informit.com/store/product.aspx?isbn=0131177052 позволяет увидеть содержание этой книги (и 2 примера главы). Как долго длится глава 20? (Просто чтобы почувствовать, насколько это может быть полезно.)
Мартин Ба

17
20-я глава длиной 10 000 строк, но автор работает над тем, как разбить ее на легкоусвояемые куски ... 8)
Тони Делрой

1
Его около 23 страниц, но с 14 изображениями. Я думаю, что вы должны получить это, вы будете чувствовать себя намного увереннее, пытаясь решить, что делать имхо.
Эмиль Врейдагс

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

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

14

Я думаю, что вам лучше создать набор командных классов, которые сопоставляются с точками API mainmodule.cpp.

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

Конечно, с одним классом из 11 KLOC код там, вероятно, сильно связан и хрупок, но создание отдельных классов команд поможет гораздо больше, чем любая другая стратегия прокси / фасад.

Я не завидую этой задаче, но со временем эта проблема только усугубится, если ее не решить.

Обновить

Я предположил бы, что образец Команды предпочтительнее Фасада.

Поддержание / организация множества различных командных классов на (относительно) монолитном Фасаде предпочтительнее. Отображение одного Фасада в файл 11 KLOC, вероятно, потребуется разбить на несколько разных групп.

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

Конечно, оба варианта лучше, чем один 11 KLOC и растущий файл.


+1 альтернатива предложенному мною решению с той же идеей: изменить API, чтобы разделить большую проблему на маленькие.
Бенуа

13

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

Одним из способов может быть начать разделение наименьшей большой функции / части на собственный файл, а затем либо включить заголовок (таким образом, превратив main.cpp в список #include, что само по себе звучит как запах кода) хотя C ++ Guru), но, по крайней мере, теперь он разбит на файлы).

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

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

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


1
+1 для вычленения и #include обратно. Если бы вы сделали это для всех 10 ветвей (немного работы, но управляемы), у вас все равно осталась бы другая проблема - проблема публикации изменений во всех ваших ветвях, но эта проблема не расширились (обязательно). Это уродливо? Да, это все еще так, но это может внести небольшую рациональность в проблему. Потратив несколько лет на техническое обслуживание и ремонт действительно большого продукта, я знаю, что техническое обслуживание сопряжено с большими трудностями. По крайней мере, извлекать уроки из этого и служить предостережением для других.
Джей

10

Rofl, это напоминает мне о моей старой работе. Кажется, что до того, как я присоединился, все было внутри одного огромного файла (также C ++). Затем они разделили его (в совершенно случайных точках с использованием включений) на три (все еще огромные файлы). Как и следовало ожидать, качество этого программного обеспечения было ужасным. Проект составил около 40 тыс. LOC. (почти не содержит комментариев, но много повторяющегося кода)

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

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


редактировать:

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

Программное обеспечение было для встроенной системы, принтера этикеток.

Еще один момент, который я должен добавить, это то, что теоретически проект был C ++. Но это был не OO, это мог быть C. Новая версия была объектно-ориентированной.


9
Каждый раз, когда я слышу "с нуля" в теме о рефакторинге, я убиваю котенка!
Кугель

Я был в очень похожей ситуации, хотя основной цикл программы, с которым мне пришлось столкнуться, составлял всего ~ 9000 LOC. И это было достаточно плохо.
AndyUK

8

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

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

Во-вторых, вам нужно взять эту рабочую версию и создать ветвь, которая управляет сборками, используя директивы предварительной обработки для разделения большого файла. Разделить компиляцию с использованием директив препроцессора JUST (#ifdefs, #include, #endifs) проще, чем перекодировать API. Это определенно проще для ваших SLA и постоянной поддержки.

Здесь вы можете просто вырезать функции, которые относятся к конкретной подсистеме в классе, и поместить их в файл, скажем mainloop_foostuff.cpp, и включить его в mainloop.cpp в нужном месте.

ИЛИ

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

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

Основная структура состоит в том, что ваш mainclass.cpp будет включать новый файл с именем MainClassComponents.cpp после блока операторов, подобного следующему:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

Первичная структура файла MainClassComponents.cpp была бы там, чтобы вырабатывать зависимости внутри подкомпонентов, как это:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

И теперь для каждого компонента вы создаете файл component_xx.cpp.

Конечно, я использую числа, но вы должны использовать что-то более логичное на основе вашего кода.

Использование препроцессора позволяет разделить вещи, не беспокоясь об изменениях API, что является кошмаром в производстве.

Как только вы закончите производство, вы сможете приступить к редизайну.


Это похоже на трудные, но изначально болезненные результаты опыта.
JBRWilkinson

На самом деле, это техника, которая использовалась в компиляторах Borland C ++ для эмуляции использования стиля Pascal для управления заголовочными файлами. Особенно, когда они сделали начальный порт своей текстовой оконной системы.
Эльф Кинг

8

Ну, я понимаю вашу боль :) Я был в нескольких таких проектах, и это не красиво. На это нет простого ответа.

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

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


4

Вам не следует беспокоиться об уменьшении размера файла, а скорее об уменьшении размера класса. Это сводится почти к тому же, но заставляет вас взглянуть на проблему под другим углом (как предполагает @Brian Rasmussen , у вашего класса, похоже, много обязанностей).


Как всегда, я хотел бы получить объяснение понижения.
Бьорн Поллекс

4

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


4

Это не ответ на большую проблему, а теоретическое решение конкретной ее части:

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

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

  • Запустите скрипт. Удалить оригинальный файл.

  • Когда вам нужно объединить из ветви, сначала воссоздайте большой файл, объединяя части вместе, сделайте объединение, а затем заново разделите его.

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


4

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

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


Я думаю, что другие предложили подобные вещи. Это коротко и по существу, и я думаю, что это может быть правильной отправной точкой для первоначальной проблемы.
Мартин Ба,

3

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

Как бы то ни было, я предлагаю отбросить всю реализацию и перегруппировать ее в новые модули, назовем эти «глобальные сервисы». «Основной модуль» будет перенаправлять только эти службы, и ЛЮБОЙ новый код, который вы пишете, будет использовать их вместо «основного модуля». Это должно быть осуществимо в разумные сроки (потому что это в основном копирование и вставка), вы не нарушаете существующий код и можете делать это по одной версии обслуживания за раз. И если у вас еще есть время, вы можете потратить его на рефакторинг всех старых зависимых модулей, чтобы также использовать глобальные сервисы.


3

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

  1. Напишите код для исчерпывающего тестирования функции в рассматриваемой программе. Похоже, у вас уже не будет этого в руках ...
  2. Определите некоторый код, который можно абстрагировать в класс помощника / утилит. Не нужно быть большим, просто что-то, что не является частью вашего «основного» класса.
  3. Рефакторинг кода, обозначенного в 2., в отдельный класс.
  4. Перезапустите тесты, чтобы убедиться, что ничего не сломано.
  5. Когда у вас будет время, перейдите к 2. и повторите, как требуется, чтобы сделать код управляемым.

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

Я также мог бы добавить:

0: купите книгу Майкла Фезерса о работе с устаревшим кодом

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


2

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

Если вы нашли работоспособное решение, выполните рефакторинг приложения соответствующим образом.

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


+1 - переписать его в свое время, хотя в противном случае кто-то может выплюнуть их пустышку.
Джон Блэк

2

Мои 0,05 евроцента:

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

При разбиении на подсистемы анализируйте места, которые наиболее изменились, и отделяйте их от неизменных частей. Это должно показать вам проблемные места. Отделите наиболее изменяющиеся части от их собственных модулей (например, DLL) таким образом, чтобы можно было сохранить целостность API модуля, и вам не нужно постоянно ломать BC. Таким образом, при необходимости можно развернуть разные версии модуля для разных веток обслуживания, оставив ядро ​​без изменений.

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

Что касается истории исходного кода, мое мнение: забудьте о новом коде. Но храните историю где-нибудь, чтобы вы могли проверить ее, если это необходимо. Бьюсь об заклад, вам не понадобится так много после начала.

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

Вот так я бы начал решать проблему, по крайней мере.


2

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



2

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


2

Я нашел это предложение самой интересной частью вашего поста:

> Файл используется и активно изменяется в нескольких (> 10) версиях нашего продукта, поэтому его очень сложно реорганизовать

Во-первых, я бы порекомендовал вам использовать систему контроля версий для разработки этих 10+ версий поддержки, поддерживающих ветвление.

Во-вторых, я бы создал десять веток (по одной для каждой из ваших версий обслуживания).

Я чувствую, что ты уже съеживаешься! Но либо ваш источник управления не работает для вашей ситуации из-за отсутствия функций, либо он используется неправильно.

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

Я был бы немного обеспокоен тем, что у вас так много в вашей функции main ().

В любых проектах, которые я пишу, я бы использовал main () только для выполнения инициализации основных объектов - таких как объект моделирования или приложения - в этих классах должна продолжаться настоящая работа.

Я также инициализирую объект регистрации приложения в main для использования во всем приложении.

Наконец, в основной я также добавляю код обнаружения утечек в блоки препроцессора, которые гарантируют, что он включен только в сборках DEBUG. Это все, что я хотел бы добавить к main (). Main () должен быть коротким!

Вы говорите, что

> Файл в основном содержит «основной класс» (диспетчеризация и координация основной внутренней работы) нашей программы.

Похоже, что эти две задачи могут быть разделены на два отдельных объекта - координатор и диспетчер работы.

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

Если вы не можете принять решение, сражайтесь изо всех сил с вашим менеджером за это - ваше приложение должно быть подвергнуто рефакторингу - и плохо под звуки этого! Не принимай за ответ!


Как я понимаю, проблема в следующем: если вы кусаете пулю и рефакторинг, вы больше не можете переносить патчи между версиями. SCC может быть идеально настроен.
Петерчен

@peterchen - именно проблема. SCCs объединяются на уровне файлов. (Трехстороннее объединение). Если вы перемещаете код между файлами, вам придется вручную переключать блоки измененного кода из одного файла в другой. (Функция GIT, о которой кто-то еще упомянул в другом комментарии, хороша для истории, а не для слияния, насколько я могу судить)
Мартин Ба,

2

Как вы уже описали, основная проблема заключается в том, чтобы сравнивать пре-сплит с пост-сплит, объединять исправления ошибок и т. Д. Инструмент вокруг этого. Это не займет много времени, чтобы жестко закодировать скрипт в Perl, Ruby и т. Д., Чтобы вырвать большую часть шума из различий пре-сплит против конкатенации пост-сплит. Делайте все возможное с точки зрения обработки шума:

  • удалить определенные строки до / во время объединения (например, включить охранники)
  • при необходимости удалите другие вещи из вывода diff

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


2
  1. Никогда не трогайте этот файл и код снова!
  2. Лечить это как то, с чем ты застрял. Начните писать адаптеры для функциональности, закодированной там.
  3. Пишите новый код в разных единицах и общайтесь только с адаптерами, которые инкапсулируют функциональность монстра.
  4. ... если только одно из вышеперечисленных невозможно, оставьте работу и получите новую.

2
+/- 0 - серьезно, где вы живете, и вы бы порекомендовали бросить работу, основываясь на таких технических деталях?
Мартин Ба,

1

«Файл, в основном, содержит« основной класс »(главная внутренняя рабочая диспетчеризация и координация) нашей программы, поэтому каждый раз, когда функция добавляется, она также влияет на этот файл и каждый раз, когда он увеличивается».

Если этот большой SWITCH (который, я думаю, существует) становится основной проблемой обслуживания, вы можете реорганизовать его для использования словаря и шаблона Command и удалить всю логику переключения из существующего кода в загрузчик, который заполняет эту карту, то есть:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();

2
Нет, большого переключателя на самом деле нет. Предложение как раз ближе всего подходит к описанию этого беспорядка :)
Martin Ba

1

Я думаю, что самый простой способ отследить историю источника при разбиении файла будет что-то вроде этого:

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

«используя любые сохраняющие историю команды копирования, которые предоставляет ваша система SCM» ... плохая вещь, которую она не предоставляет
Martin Ba

Жаль. Это само по себе звучит как хорошая причина для перехода на что-то более современное. :-)
Кристофер Кройциг

1

Я думаю, что я сделал бы в этой ситуации немного пули и:

  1. Выясните, как я хотел разделить файл (на основе текущей версии разработки)
  2. Установите административную блокировку для файла («Никто не трогает mainmodule.cpp после 5 вечера пятницы !!!»
  3. Проведите долгие выходные, применяя это изменение к> 10 версиям обслуживания (от самой старой до самой новой), вплоть до текущей версии.
  4. Удалите mainmodule.cpp из всех поддерживаемых версий программного обеспечения. Это новый век - mainmodule.cpp больше нет.
  5. Убедите руководство, что не следует поддерживать более одной версии программного обеспечения для обслуживания (по крайней мере, без большого контракта на поддержку $$$). Если у каждого из ваших клиентов есть свои уникальные версии .... уууууууу. Я бы добавил директивы компилятора, а не пытался поддерживать более 10 вилок.

Отслеживание старых изменений в файле просто решается с помощью вашего первого комментария о регистрации, который говорит что-то вроде «split from mainmodule.cpp». Если вам нужно вернуться к чему-то недавнему, большинство людей запомнят это изменение, если через 2 года комментарий укажет им, где искать. Конечно, насколько ценным будет вернуться назад, чтобы узнать, кто изменил код и почему?

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