Введение
В заключительной статье цикла соберём вместе концепции из предыдущих девяти статей в цельную архитектуру — спроектируем расширяемый текстовый редактор, где функциональность форматирования, экспорта и подсветки синтаксиса реализована через систему плагинов, а не зашита напрямую в основное приложение. В статье разберём общую структуру такого приложения и ключевые архитектурные решения, обеспечивающие его реальную расширяемость.
Концепция
Plugin-based архитектура редактора обычно строится вокруг нескольких категорий расширяемых точек (extension points): плагины форматирования/экспорта , плагины подсветки синтаксиса для разных языков программирования, и плагины-команды, добавляющие новые пункты в меню или панель инструментов. Центральный PluginManager отвечает за сканирование директории плагинов, фильтрацию по метаданным , безопасную загрузку и предоставление приложению унифицированного реестра доступных расширений по категориям.
Пример кода
// pluginmanager.h — центральный реестр всех загруженных плагинов приложения
#pragma once
#include <QObject>
#include <QPluginLoader>
#include <QDir>
#include <QHash>
#include "iexportplugin.h"
#include "isyntaxhighlighterplugin.h"
#include "ieditorcommandplugin.h"
class PluginManager : public QObject
{
Q_OBJECT
public:
explicit PluginManager(QObject *parent = nullptr) : QObject(parent) {}
void loadPluginsFrom(const QString &directory)
{
QDir dir(directory);
for (const QString &fileName : dir.entryList(QDir::Files)) {
const QString filePath = dir.absoluteFilePath(fileName);
tryLoadPlugin(filePath);
}
qDebug() << "Загружено плагинов экспорта:" << m_exportPlugins.size()
<< "| подсветки синтаксиса:" << m_highlighterPlugins.size()
<< "| команд:" << m_commandPlugins.size();
}
QList<IExportPlugin *> exportPlugins() const { return m_exportPlugins; }
QList<ISyntaxHighlighterPlugin *> highlighterPluginsFor(const QString &fileExtension) const
{
QList<ISyntaxHighlighterPlugin *> result;
for (auto *plugin : m_highlighterPlugins) {
if (plugin->supportedExtensions().contains(fileExtension)) {
result.append(plugin);
}
}
return result;
}
QList<IEditorCommandPlugin *> commandPlugins() const { return m_commandPlugins; }
private:
void tryLoadPlugin(const QString &filePath)
{
auto loader = std::make_unique<QPluginLoader>(filePath);
const QJsonObject metaData = loader->metaData().value("MetaData").toObject();
const QString category = metaData.value("category").toString();
// Загружаем код плагина в память ТОЛЬКО для категорий, которые приложение знает
if (category.isEmpty()) {
return; // не Qt-плагин или плагин без объявленной категории — пропускаем
}
QObject *instance = loader->instance();
if (!instance) {
qWarning() << "Ошибка загрузки" << filePath << ":" << loader->errorString();
return;
}
bool recognized = false;
if (auto *exporter = qobject_cast<IExportPlugin *>(instance)) {
m_exportPlugins.append(exporter);
recognized = true;
}
if (auto *highlighter = qobject_cast<ISyntaxHighlighterPlugin *>(instance)) {
m_highlighterPlugins.append(highlighter);
recognized = true;
}
if (auto *command = qobject_cast<IEditorCommandPlugin *>(instance)) {
m_commandPlugins.append(command);
recognized = true;
}
if (recognized) {
m_loaders.push_back(std::move(loader)); // удерживаем loader, иначе библиотека выгрузится
} else {
loader->unload();
}
}
QList<IExportPlugin *> m_exportPlugins;
QList<ISyntaxHighlighterPlugin *> m_highlighterPlugins;
QList<IEditorCommandPlugin *> m_commandPlugins;
std::vector<std::unique_ptr<QPluginLoader>> m_loaders;
};
// Использование в главном окне редактора
#include "pluginmanager.h"
#include <QMenu>
void EditorWindow::initializePlugins()
{
m_pluginManager.loadPluginsFrom(QCoreApplication::applicationDirPath() + "/plugins");
auto *exportMenu = menuBar()->addMenu("Экспорт");
for (auto *plugin : m_pluginManager.exportPlugins()) {
exportMenu->addAction(plugin->formatName(), [this, plugin]() {
exportCurrentDocument(plugin);
});
}
auto *toolsMenu = menuBar()->addMenu("Команды");
for (auto *command : m_pluginManager.commandPlugins()) {
toolsMenu->addAction(command->commandLabel(), [command, this]() {
command->execute(currentDocumentContext());
});
}
}
void EditorWindow::onDocumentOpened(const QString &filePath)
{
const QString extension = QFileInfo(filePath).suffix();
const auto highlighters = m_pluginManager.highlighterPluginsFor(extension);
if (!highlighters.isEmpty()) {
applyHighlighter(highlighters.first());
}
}
Пояснения к коду
PluginManager::tryLoadPlugin() объединяет несколько идей из предыдущих статей цикла: предварительная проверка категории через метаданные до полной загрузки , множественная проверка через qobject_cast на разные категории интерфейсов, поскольку один плагин теоретически мог бы реализовывать сразу несколько ролей (расширение паттерна из статьи 181), и явное удержание QPluginLoader в векторе m_loaders — без этого хранения объект loader был бы разрушен при выходе из функции tryLoadPlugin, что по умолчанию выгружает связанную библиотеку, делая использованными ранее полученные указатели на интерфейсы недействительными.
EditorWindow::initializePlugins() показывает, как унифицированный реестр плагинов напрямую транслируется в реальные элементы пользовательского интерфейса — каждый плагин экспорта автоматически получает пункт в меню «Экспорт», и каждый плагин-команда — пункт в меню «Команды», без какого-либо знания основного приложения о конкретных, заранее перечисленных реализациях. highlighterPluginsFor() демонстрирует диспетчеризацию по поддерживаемым расширениям файлов, выбирая подходящий плагин подсветки синтаксиса динамически при открытии конкретного документа.
Подводные камни
- Отсутствие явного порядка приоритета, если несколько плагинов заявляют поддержку одного и того же расширения файла (например, два разных плагина подсветки синтаксиса для
.cpp) —highlighterPluginsFor()в примере просто возвращает первый найденный без какой-либо логики выбора «лучшего» — для реальной системы нужен явный механизм приоритета (например, через дополнительное поле в метаданных, или явный выбор пользователем при конфликте). - Отсутствие реестра уже загруженных плагинов по идентификатору при повторном сканировании директории (например, при добавлении новых плагинов пользователем во время работы приложения, без перезапуска) — простое повторное вызывание
loadPluginsFrom()без проверки уже загруженных путей привело бы к дублированию плагинов в списках; для динамического добавления новых плагинов в рантайме нужна явная инкрементальная логика, а не повторный полный проход. - Жёсткая связь между UI-элементами (пункты меню) и интерфейсом плагина в
initializePlugins(). Если в будущем плагины должны получить более богатый способ интеграции с UI (например, собственные иконки, горячие клавиши, подменю), текущий простой подход «одна команда = один пункт меню» придётся расширять — стоит с самого начала проектировать интерфейс плагина команд с запасом на такие детали оформления, а не добавлять их как breaking change впоследствии. - Отсутствие изоляции между плагинами разных категорий в случае сбоя одного из них. Если, например, один плагин подсветки синтаксиса содержит баг, приводящий к зависанию при определённом содержимом файла, это может затронуть отзывчивость всего приложения при попытке открыть проблемный файл — для критичных продакшен-сценариев стоит сочетать архитектуру этой статьи с подходами изоляции и тайм-аутов из статьи 185, особенно для категорий плагинов, вызываемых синхронно в основном потоке UI (как подсветка синтаксиса, в отличие от экспорта, который естественно выполняется асинхронно).