Архитектура приложения, которое открывает несколько документов (проектов)

Я работаю над приложением САПР на основе Qt и пытаюсь выяснить архитектуру приложения. Приложение может загружать несколько проектов с планами, сечениями и т. Д. И показывать эти чертежи в выделенных видах. Есть на проект и глобальные конфигурации.

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

class CADApplication Q_DECL_FINAL: public QApplication {
Q_OBJECT
public:
explicit CADApplication(int &args, char **argv);
virtual ~CADApplication();
...
ProjectManager* projectManager() const;
ConfigManager* configManager() const;
UndoManager*  undoManager() const;

protected:
const QScopedPointer<ProjectManager> m_projectManager;
const QScopedPointer<ConfigManager> m_configManager;
...
};

«Менеджеры» создаются в CADApplicationконструктор. Они несут ответственность за функциональность, связанную с загруженными проектами (ProjectManager), глобальные параметры конфигурации (ConfigManager) и так далее.

Есть также виды проектов, диалоговые окна параметров конфигурации и другие объекты, которым может потребоваться доступ к «менеджерам».

Для того, чтобы получить текущий проект, SettingsDialog нуждается в:

#include "CADApplication.h"#include "ProjectManager.h"...
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)
{
...
Project* project = qApp->projectManager()->currentProject();
...
}

Что мне нравится в этом подходе, так это то, что он следует парадигме RAII. «Менеджеры» создаются и уничтожаются при создании / уничтожении приложения.

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

Я сделал некоторые исследования. Кажется, что есть и несколько других подходов, которые предполагают использование синглетонов. OpenToonz использует TProjectManager синглтон:

class DVAPI TProjectManager {
...
public:
static TProjectManager *instance();
...
};

TProjectManager* TProjectManager::instance() {
static TProjectManager _instance;
return &_instance;
}

В каждом файле, где им нужен доступ к менеджеру проекта:

#include "toonz/tproject.h"...

TProjectManager *pm = TProjectManager::instance();
TProjectP sceneProject = pm->loadSceneProject(filePath);

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

16

Решение

Я работаю в VFX, который немного отличается от CAD, но не слишком отличается, по крайней мере, для моделирования. Там я нашел очень полезным вращать дизайн приложения для моделирования вокруг шаблона команды. Конечно, вам не обязательно делать некоторые ICommand интерфейс для этого. Вы могли бы просто использовать std::function и лямбды, например.

Устранение / уменьшение центральных зависимостей для экземпляров одного объекта

Тем не менее, я делаю вещи немного иначе для состояния приложения, потому что я предпочитаю больше «тянуть» парадигму вместо «толчка» для типа вещей, которые вы делаете (я постараюсь объяснить это лучше ниже с точки зрения что я подразумеваю под «push / pull» *), так что вместо того, чтобы загружать вещи в «мире», получая доступ к этим нескольким экземплярам центрального менеджера и говоря им, что делать, немногие центральные менеджеры как бы «получают доступ к миру» (не напрямую) и выяснить, что делать.

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

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

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

введите описание изображения здесь

Я предлагаю сортировать / переворачивать сообщение (разветвляться от одного центрального экземпляра ко многим):

введите описание изображения здесь

И системе отмены больше не говорят, что делать больше всем остальным. Он выясняет, что делать, получая доступ к сцене (в частности, к компонентам транзакции). На широком концептуальном уровне я думаю об этом в терминах «толкай / толкай» (хотя меня обвиняли в том, что я немного путаюсь с этой терминологией, но я не нашел лучшего способа описать или подумать об этом — как ни странно, это был коллега, который первоначально описывал это как «извлечение» данных, а не как «подталкивание» в ответ на мои неудачные попытки описать, как система работает в команде, и его описание было настолько интуитивно понятным для меня, что оно застряло со мной с тех пор). С точки зрения зависимостей между экземплярами объектов (не типами объектов) это своего рода замена разветвления на разветвление.

Минимизация знаний

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

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

Позволяет «миру» работать с меньшими знаниями. Мир не должен знать о полномасштабных менеджерах отмены, и менеджерам отмены не нужно знать обо всем мире. Теперь обоим нужно знать только об этих простых компонентах транзакций.

Отменить Системы

В частности, для систем отмены я достигаю такого рода «инверсии связи», когда каждый объект сцены («Вещь» или «Сущность») записывает в свой собственный компонент локальной транзакции. Затем система отмены периодически (например, после выполнения команды) пересекает сцену и собирает все компоненты транзакции из каждого объекта в сцене и объединяет ее в одну запись, отменяемую пользователем, например, так (псевдокод):

void UndoSystem::process(Scene& scene)
{
// Gather the transaction components in the scene
// to a single undo entry.
local undo_entry = {}

// Our central system loop. We loop through the scene
// and gather the information for the undo system to
// to work instead of having everything in the scene
// talk to the undo system. Most of the system can
// be completely oblivious that this undo system even
// exists.
for each transaction in scene.get<TransactionComponent>():
{
undo_entry.push_back(transaction);

// Clear the transaction component since we've recorded
// it to the undo entry so that entities can work with
// a fresh (empty) transaction.
transaction.clear();
}

// Record the undo entry to the undo system's history
// as a consolidated user-undoable action. I used 'this'
// just to emphasize the 'history' is a member of our
// undo system.
this->history.push_back(undo_entry);
}

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

Также вы можете легко иметь более одного системного экземпляра отмены, используя этот подход. Например, это может немного сбивать с толку, если пользователь работает в «Сцена B» или «Проект B» и нажимает «Отменить» только для отмены изменения в «Сцена A / Проект А», в котором они не работают сразу. не могут видеть, что происходит, они могут даже случайно отменить изменения, которые они хотели сохранить. Это было бы похоже на то, как я ударил отменить в Visual Studio во время работы в Foo.cpp и случайно отменяет изменения в Bar.cpp, В результате вам часто требуется более одного отмены экземпляра системы / менеджера в программном обеспечении, которое позволяет создавать несколько документов / проектов / сцен. Они не обязательно должны быть одиночными, не говоря уже об объектах всего приложения, и часто должны быть объектами document / project / scene-local. Этот подход позволит вам легко сделать это, а также передумать позже, поскольку он минимизирует количество зависимостей в системе от вашего менеджера отмены (возможно, от него зависит только одна или две команды в вашей системе *).

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

Избегание фан-ин в экземплярах центрального объекта

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

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

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

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

введите описание изображения здесь

… или это:

введите описание изображения здесь

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

Крупномасштабные проекты

Помимо всего прочего, при изучении различных методологий проектирования, шаблонов и показателей SE, я обнаружил, что наиболее эффективный способ позволить системам масштабироваться по сложности — это изолировать здоровенные разделы в «их собственном мире», полагаясь на минимальное количество информации для работы. «Полная развязка» такого рода, которая минимизирует не только конкретную информацию, необходимую для работы чего-либо, но и минимизирует абстрактную информацию, является, помимо всего прочего, наиболее эффективным способом, который я нашел, чтобы позволить системам масштабироваться без чрезмерной нагрузки. разработчики, поддерживающие их. Для меня это огромный предупреждающий знак, когда, скажем, у вас есть разработчик, который знает NURB на поверхности и может накапливать впечатляющие демонстрации, связанные с ними, но он спотыкается об ошибках в вашей системе в своей лофтовой реализации не потому, что его реализация NURBs неверна , но потому что он имеет дело с центральными состояниями всего приложения неправильно.

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

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

Это может быть полезным упражнением, чтобы просто сесть с частью системы и спросить: «Сколько информации из внешнего мира мне нужно понять, чтобы реализовать / изменить эту вещь?»

введите описание изображения здесь

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

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

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

«Менеджеры» против «Систем»

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

Вместо этого я посчитал полезным отдать предпочтение тому, что я хочу называть «системами», просто взяв это на вооружение из архитектур, популярных в разработке игр. Системы «тянут», как я хочу это назвать. Никто не говорит им конкретно, что делать. Вместо этого они выясняют это самостоятельно, получая доступ к миру, извлекая из него данные, чтобы выяснить, что делать и делать. Мир не имеет к ним доступа. Они перебирают вещи и что-то делают. Вы можете применить эту стратегию ко многим своим «менеджерам», чтобы весь мир не пытался с ними разговаривать.

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

введите описание изображения здесь

Порядок уничтожения

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

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

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

Entity-Component Systems

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

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

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

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

С одиночками

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

Например, вместо доступа к менеджеру отмены через CADApplicationвы могли бы сделать:

// In header:
// Returns the undo manager:
UndoManager& undoManager();

// Inside source file:
#include "CADApplication.h"
UndoManager& undoManager()
{
return *CADApplication::instance()->undoManager();
}

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

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

Модульное тестирование

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

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

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

func(a, b)

Тогда это может быть легко проверить тщательно, потому что мы можем рассуждать о том, какие комбинации a а также b привести к различным случаям для func обрабатывать. Если он становится методом, то он вводит один невидимый (self / this) параметр:

// * denotes invisible parameter
method(*self, a, b)

… но это все еще может быть довольно легко рассуждать, если наш объект не содержит слишком много состояния, которое может повлиять method (однако, даже объект, который не зависит ни от кого другого, может быть трудно проверить, если он имеет, скажем, 25 эклектичных переменных-членов). И это невидимый параметр, но не совсем скрытый. Это становится очевидным, когда вы звоните foo.method(...) тот foo является входом в функцию. Но если мы начнем внедрять много сложных объектов или если глобальные / синглтоны будут зависеть, то у нас могут быть всевозможные действительно скрытые параметры, о которых мы можем даже не знать, если мы не проследим реализацию функции / метода:

method(*self, a, b, *c, *d, *e, *f, *g, *h, *i, *j)

… и это может быть действительно трудно проверить, и мы не можем понять, исчерпали ли мы все возможные комбинации ввода / состояния, которые приводят к уникальным случаям для обработки. Это еще одна причина, по которой я бы рекомендовал этот подход — превращать ваших «менеджеров» в «системы» и избегать перехода на отдельные экземпляры объектов, если ваше приложение достигает достаточно большого масштаба.

Теперь ECS, который я показал выше, у которого есть один центральный и обобщенный «ECS Manager» (хотя и внедренный, а не синглтон), может показаться, что он не будет отличаться, но разница в том, что ECS не имеет собственного сложного поведения. , Это довольно тривиально, по крайней мере, в смысле отсутствия каких-либо непонятных крайних случаев. Это обобщенная «база данных». Он хранит данные в виде компонентов, которые системы читают как входные данные и используют для вывода чего-либо.

Системы просто используют его для извлечения определенных типов компонентов и объектов, которые их содержат. В результате, как правило, нет неясных крайних случаев, с которыми приходится иметь дело, поэтому часто вы можете протестировать все, просто сгенерировав данные для компонентов, которые система должна обработать (например: компоненты транзакций, если вы тестируете свою систему отмены), чтобы увидеть, это работает правильно. Удивительно, но я обнаружил, что ECS проще тестировать по сравнению с прежней кодовой базой (у меня изначально были опасения по поводу попытки использовать ECS в визуальной области FX, которая, насколько я знаю среди всех крупных конкурентов, никогда не была предпринята раньше). Бывшая кодовая база действительно использовала несколько синглетонов, но в большинстве случаев все еще использовала DI. Тем не менее, ECS было еще проще тестировать, так как единственное, что вам нужно сделать, чтобы проверить правильность системы, — это создать в качестве входных данных некоторые компоненты нужного типа (например, аудиокомпоненты для аудиосистемы) и убедиться, что они обеспечивают правильный вывод после их обработки.

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

Плюсы и минусы

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

Плюсы:

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

Минусы:

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

Другие решения

Возможно, проще всего последовать примеру Qt, измененному в наше время. Qt использует глобальный макрос для ссылки на экземпляр, например, в qapplication.h:

#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))

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

// cadapplication.h
...

#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<CADApplication*>(QCoreApplication::instance()))

Тогда, например, pMgr будет выглядеть так:

#define pMgr (qApp->projectManager())

Я хотел бы рассмотреть, однако, отсутствие пространств имен и глобального pMgr и подобные макросы плохо пахнут. Вместо макроса используйте встроенную функцию в пространстве имен:

// cadapplication.h
...
namespace CAD {
class Application : public QApplication {
ProjectManager m_projectManager; /* holding the value has less overhead */
public:
inline ProjectManager* projectManager() const { return &m_projectManager; }
...
};

inline ProjectManager* pMgr() {
return static_cast<CAD::Application*>(QCoreApplication::instance())->projectManager();
}
}

#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<CAD::Application*>(QCoreApplication::instance()))

Затем:

#include "projectmanager.h"...
CAD::pMgr()->doSomething();
/* or */
using namespace CAD;
pMgr()->doSomething();

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

// cadapplication.h

namespace CAD {
...
namespace detail {
struct ProjectManagerFwd {
inline ProjectManager* operator->() const {
return qApp->projectManager();
}
inline ProjectManager& operator*() const {
return *(qApp->projectManager());
}
};
}
extern detail::ProjectManagerFwd pMgr;
}

// cadapplication.cpp
...
detail::ProjectManagerFwd pMgr;
...

Затем:

#include "cadapplication.h"...
CAD::pMgr->doSomething();
/* or */
using namespace CAD;
pMgr->doSomething();

Ни в коем случае не требуется специальный заголовок.

Даже если вы не используете пространства имен (почему ?!), глобальные переменные все равно должны находиться в пространстве имен (будь то pMgr функция или pMgr пример).

4

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

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

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

ИМО каждый проект должен иметь свой собственный менеджер команд.

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

3

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

class Status{ /* ... */ };

class ManagerPrivate
{
public:
static ManagerPrivate & instance();
void doThis();
void doThat();
void doSomethingElse();

// etc ...

Status status() const;
private:
Status _status;
};

Прокси для этого менеджера может быть таким:

class Manager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doThis();
ManagerPrivate::instance().doThat();
return ManagerPrivate::instance().status();
}
//...
};

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

class BaseManager
{
public:
virtual ~BaseManager() = default;
virtual Status doSomething() = 0;
};

class ManagerA : public BaseManager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doThis();
ManagerPrivate::instance().doThat();
return ManagerPrivate::instance().status();
}
};

class ManagerB : public BaseManager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doSomethingElse();
return ManagerPrivate::instance().status();
}
};

или один класс фасадов, который охватывает более одного синглтона, и так далее.

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

void someFunction()
{
//...

Status theManagerStatus = ManagerX().doSomething();

//...
}

Инверсия контроля — все еще выполнимая особенность:

BaseManager * theManagerToUse()
{
if(configuration == A)
{
return new ManagerA();
}
else if(configuration == B)
{
return new ManagerB();
}
// etc ...
}
3