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

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

Приложение представлено глобальным объектом, полученным из 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 объект 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);

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

Ответ 1

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

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

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

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

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

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

enter image description here

Я предлагаю sorta flipping/invverting communication (выключение из одного центрального экземпляра во многие экземпляры):

enter image description here

И отменить систему больше не говорят, что делать больше всем. Он определяет, что делать, обращаясь к сцене (в частности, к компонентам транзакции). На широком концептуальном уровне я думаю об этом с точки зрения "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 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/Проект A", с которой они не работают. Если они не могут видеть, что происходит, они могут даже непреднамеренно отменять изменения, которые они хотели сохранить. Это будет похоже на то, что я Foo.cpp отмену в Visual Studio во время работы в Foo.cpp и случайно Bar.cpp изменения в Bar.cpp. В результате вы часто нуждаетесь в более чем одном экземпляре системы/менеджера в программном обеспечении, который позволяет использовать несколько документов/проектов/сцен. Они не обязательно должны быть одноточиями, не говоря уже об объектах, охватываемых всей средой, и вместо этого часто должны быть объектами документа/проекта/сцены. Этот подход легко позволит вам это сделать, а также изменить свое мнение позже, поскольку он минимизирует количество зависимостей в системе от вашего менеджера отмены (возможно, только одна или две команды в вашей системе должны зависеть от нее *).

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

Избегание веерного входа в центральные объекты

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

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

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

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

enter image description here

... или это:

enter image description here

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

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

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

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

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

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

enter image description here

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

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

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

"Менеджеры" и "Системы"

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

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

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

enter image description here

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

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

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

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

Системные системы Entity-Component

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

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

В качестве очень простого примера вы можете просто создать одну центральную абстракцию для объектов сцены, таких как ISceneObject (или даже ABC), с помощью метода виртуальных 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 и доступа к тому, что от этого требуется.

Тестирование устройства

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

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

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

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 I, у которого есть один центральный и обобщенный "ECS Manager" (хотя и введенный, а не один), может показаться, что это ничем не отличается, но разница в том, что ECS не имеет сложного поведения, Это довольно тривиально, по крайней мере, в смысле отсутствия каких-либо неясных краевых случаев. Это обобщенная "база данных". Он хранит данные в виде компонентов, которые системы считывают как входные данные и используют для вывода чего-либо.

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

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

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

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

Плюсы:

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

Минусы:

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

Ответ 2

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

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

В вашем случае мы знаем, что тип глобального приложения singleton - это 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 использование 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 экземпляр).

Ответ 3

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

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 ...
}

Ответ 4

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

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

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

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

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