Как лучше всего перейти от шаблонного беспорядка к чистой архитектуре классов (C ++)? - PullRequest
1 голос
/ 27 ноября 2008

Предполагается большая библиотека шаблонов с около 100 файлами, содержащими около 100 шаблонов с общим числом строк кода более 200 000. Некоторые из шаблонов используют множественное наследование, чтобы сделать использование самой библиотеки довольно простым (то есть наследовать от некоторых базовых шаблонов и выполнять только определенные бизнес-правила).

Все, что существует (выросло за несколько лет), «работает» и используется для проектов.

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

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

Есть ли проверенный способ выполнить эту задачу? С чего бы начать?

Ответы [ 8 ]

13 голосов
/ 27 ноября 2008

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

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

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

Удаление шаблонов приведет только к большим зависимостям, а не к меньшему. Добавленная типобезопасность шаблонов может быть использована для обнаружения множества ошибок во время компиляции (для этой цели обильно посыпьте ваш код static_assert)

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

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

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

7 голосов
/ 27 ноября 2008

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

Кроме этого, «разделяй и властвуй»

3 голосов
/ 27 ноября 2008

Написать юнит-тесты.

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

Это как минимум один совет.

Edit:

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

2 голосов
/ 27 ноября 2008

Некоторые моменты (но обратите внимание: это , а не действительно зло. Если вы хотите перейти на не шаблонный код, это может помочь):


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

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

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

Тип поиска черт . Существует много кода, проверяющего, существует ли какой-либо typedef или какой-либо метод существует в типичном шаблонном коде. Абстрактные базовые классы решают эти две проблемы, используя чисто виртуальные методы и наследуя typedefs для базы. Часто, typedefs нужны только для запуска отвратительных функций, таких как SFINAE , которые также были бы лишними.

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

Функция поиска объектов . Если вы обнаружите, что в вашем коде используются функциональные объекты, вы можете изменить их на абстрактные базовые классы, и у вас есть что-то вроде void run(); для их вызова (или, если вы хотите продолжать использовать operator(), лучше так! Это также может быть виртуальным). ).

2 голосов
/ 27 ноября 2008

Ну, проблема в том, что шаблонное мышление сильно отличается от объектно-ориентированного наследования. Трудно ответить на что-либо еще, кроме как «перепроектировать все это и начать с нуля».

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

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

1 голос
/ 28 ноября 2008

Как я понимаю, вас больше всего беспокоит время сборки и ремонтопригодность вашей библиотеки?

Во-первых, не пытайтесь "исправить" все сразу.

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

В-третьих, сначала рассмотрите более дешевые исправления: например, более быстрые машины или распределенные инструменты сборки. По крайней мере, добавьте всю оперативную память, которую возьмут платы, и выбросьте старые диски. Это делает разницу. Один диск для ОС, один диск для сборки - это дешевый мужской RAID.

Хорошо ли задокументирована библиотека? Это ваш лучший шанс сделать это. Найдите такие инструменты, как doxygen, которые помогут вам создать такую ​​документацию.

Все считали? Хорошо, теперь некоторые предложения по времени сборки;)


Понимание C ++ модели сборки : каждый .cpp компилируется индивидуально. Это означает, что много .cpp файлов с большим количеством заголовков = огромная сборка. Однако это НЕ совет, чтобы поместить все в один файл .cpp! Однако, одна хитрость (!), Которая может значительно ускорить сборку, - это создать один файл .cpp, который включает в себя кучу файлов .cpp, и передавать только этот «главный» файл компилятору. Вы не можете делать это вслепую - вам нужно понимать типы ошибок, которые это может привести.

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

Использовать предварительно скомпилированные заголовки . (лучше масштабируется на быстрых машинах, см. выше)

Проверьте политику включения заголовка . Хотя каждый файл должен быть «независимым» (т. Е. Включать в себя все, что ему нужно, чтобы кто-то еще его включил), не включайте его свободно. К сожалению, я еще не нашел инструмент для поиска ненужных операторов #incldue, но он может помочь потратить некоторое время на удаление неиспользуемых заголовков в файлах «горячей точки».

Создание и использование предварительных объявлений для шаблонов, которые вы используете. Часто вы можете включить заголовок с объявлениями forwad во многих местах и ​​использовать полный заголовок только в нескольких конкретных. Это может очень помочь компилировать время. Проверьте заголовок <iosfwd>, как стандартная библиотека делает это для потоков ввода / вывода.

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

// .h
template <typename FLOAT> // float or double only
FLOAT CalcIt(int len, FLOAT * values) { ... }

Вы можете объявить перегрузки в заголовке и переместить шаблон в тело:

// .h
float CalcIt(int len, float * values);
double CalcIt(int len, double * values);

// .cpp
template <typename FLOAT> // float or double only
FLOAT CalcItT(int len, FLOAT * values) { ... }

float CalcIt(int len, float * values) { return CalcItT(len, values); }
double CalcIt(int len, double * values) { return CalcItT(len, values); }

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

Проверьте, может ли идиома PIMPL перемещать код из заголовков в файлы .cpp.

Общее правило, за которым скрывается , отделяет интерфейс вашей библиотеки от реализации . Используйте комментарии, detail namesapces и отдельные .impl.h заголовки, чтобы мысленно и физически изолировать то, что должно быть известно извне, от того, как это достигнуто. Это раскрывает реальную ценность вашей библиотеки (действительно ли она инкапсулирует сложность?) И дает вам возможность сначала заменить «легкие цели».


Более конкретный совет - и насколько он полезен - во многом зависит от фактической библиотеки.

Удачи!

0 голосов
/ 27 ноября 2008

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

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

0 голосов
/ 27 ноября 2008

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

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

...