C ++ и модульность: где я должен провести черту? - PullRequest
3 голосов
/ 26 июня 2011

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

Возьмем, к примеру, разработку 2D игрового движка на C ++.

Теперь можно, конечно, создать очень модульную систему, используя интерфейсы практически для всего: от модуля визуализации (Interface Renderer Class -> Dummy, OpenGL, DirectX, SDL и т. Д.) До аудио и управления входами.

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

Как мне создать такой работающий двигатель?

Я не хочу снижать ограничения для своего двигателя с точки зрения производительности (максимально возможного количества объектов, частиц и т. Д.), Чтобы иметь исключительно модульную систему, работающую в фоновом режиме. Это важно, потому что я также хотел бы ориентироваться на мобильные платформы, где мощность процессора и память ограничены.

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

А вот и мои основные вопросы:

  • Где я должен провести грань между согласованностью и производительностью при модульном программировании?

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

Ответы [ 4 ]

3 голосов
/ 26 июня 2011

Есть много способов сохранить ваш код модульным без использования единственного класса "interface".

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

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

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

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

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

2 голосов
/ 26 июня 2011

Сначала прототип, затем позвольте появиться границам интерфейса.

Упреждающий дизайн интерфейса может затруднить кодирование

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

Границы интерфейса возникают из рабочего кода.

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

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

Поэтому я согласен с предложением @ jdv-Jan de Vaan о том, что первое, что нужно сделать, это взорвать самую короткую читаемую программу, которая работает.(Это отличается от самой короткой программы, которая работает. Конечно, даже в самом начале существует некоторый минимальный дизайн интерфейса.) Я хочу добавить, что после этого следует дизайн интерфейса.То есть, получив простой код, вы можете преобразовать его в интерфейсы, чтобы сделать его еще короче и более читабельным.Если вам нужны интерфейсы для переносимости, я бы не стал этого делать, пока у вас не будет кода для двух или более платформ.Тогда границы интерфейса будут возникать естественным (и тестируемым) образом, когда станет ясно, какие функции можно использовать как есть для обоих, а какие должны иметь несколько реализаций, скрытых за интерфейсами.

1 голос
/ 26 июня 2011

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

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

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

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

1 голос
/ 26 июня 2011

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

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

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

...