Система плагинов

Архитектура plugin-based приложения: пример расширяемого редактора

1 просмотров

Введение

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

Концепция

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 (как подсветка синтаксиса, в отличие от экспорта, который естественно выполняется асинхронно).