Что делать с исходным файлом C ++ из 11000 строк? - PullRequest
226 голосов
/ 01 сентября 2010

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

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

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

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

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

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

Ответы [ 34 ]

10 голосов
/ 01 сентября 2010

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

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

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


edit:

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

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

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

8 голосов
/ 01 сентября 2010

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

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

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

8 голосов
/ 01 сентября 2010

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

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

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

Здесь вы можете просто вырезать функции, которые относятся к определенной подсистеме в классе, и поместить их в файл, например 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, что является кошмаром впроизводство.

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

4 голосов
/ 01 сентября 2010

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

4 голосов
/ 02 сентября 2010

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

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

4 голосов
/ 01 сентября 2010

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

4 голосов
/ 02 сентября 2010

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

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

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

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

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

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

3 голосов
/ 07 сентября 2010

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

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

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

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

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

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

3 голосов
/ 01 сентября 2010

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

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

2 голосов
/ 01 сентября 2010

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...