Введение
Хорошо спроектированный интерфейс плагина — основа долговечной системы расширений: он должен быть достаточно стабильным, чтобы сторонние разработчики могли писать плагины, не боясь, что следующая минорная версия приложения сломает совместимость, но достаточно гибким, чтобы покрывать реальные потребности расширения функциональности. В статье разберём принципы проектирования таких интерфейсов на более развёрнутом примере — системе плагинов экспорта документов в разные форматы.
Концепция
Хороший интерфейс плагина обычно содержит: метод самоидентификации (имя, версия плагина), метод проверки применимости (может ли конкретный плагин обработать данный конкретный случай — не все плагины экспорта могут работать с любым типом документа), и собственно метод выполнения работы. Важно избегать слишком «широких» интерфейсов с десятками методов, большинство из которых не нужны конкретной реализации — лучше декомпозировать на несколько меньших, не обязательных интерфейсов (опциональные возможности плагина проверяются дополнительным qobject_cast на необязательный, дополняющий интерфейс).
Пример кода
// iexportplugin.h — базовый интерфейс, обязательный для всех export-плагинов
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
class IExportPlugin
{
public:
virtual ~IExportPlugin() = default;
virtual QString formatName() const = 0;
virtual QString fileExtension() const = 0;
// Позволяет плагину отказаться от обработки конкретного документа
// (например, формат не поддерживает векторную графику, а документ её содержит)
virtual bool canExport(const QVariantMap &documentMetadata) const = 0;
virtual bool exportToFile(const QString &filePath, const QByteArray &documentData) = 0;
};
#define IExportPlugin_iid "com.mycompany.myapp.IExportPlugin/1.0"
Q_DECLARE_INTERFACE(IExportPlugin, IExportPlugin_iid)
// Необязательный дополняющий интерфейс — не все плагины обязаны его реализовывать
class IExportProgressReporting
{
public:
virtual ~IExportProgressReporting() = default;
virtual void setProgressCallback(std::function<void(int)> callback) = 0;
};
#define IExportProgressReporting_iid "com.mycompany.myapp.IExportProgressReporting/1.0"
Q_DECLARE_INTERFACE(IExportProgressReporting, IExportProgressReporting_iid)
// pdfexportplugin.h — реализация, поддерживающая и базовый, и опциональный интерфейс
#pragma once
#include "iexportplugin.h"
#include <QObject>
#include <functional>
class PdfExportPlugin : public QObject, public IExportPlugin, public IExportProgressReporting
{
Q_OBJECT
Q_PLUGIN_METADATA(IID IExportPlugin_iid)
Q_INTERFACES(IExportPlugin IExportProgressReporting)
public:
QString formatName() const override { return "PDF документ"; }
QString fileExtension() const override { return "pdf"; }
bool canExport(const QVariantMap &documentMetadata) const override
{
// PDF поддерживает практически любой тип контента — почти всегда true
return true;
}
bool exportToFile(const QString &filePath, const QByteArray &documentData) override
{
for (int progress = 0; progress <= 100; progress += 25) {
if (m_progressCallback) m_progressCallback(progress);
}
return true; // реальная логика экспорта в PDF здесь
}
void setProgressCallback(std::function<void(int)> callback) override
{
m_progressCallback = std::move(callback);
}
private:
std::function<void(int)> m_progressCallback;
};
// Использование: основное приложение проверяет наличие опционального интерфейса
void exportDocument(QObject *pluginInstance, const QString &filePath, const QByteArray &data)
{
auto *exporter = qobject_cast<IExportPlugin *>(pluginInstance);
if (!exporter) return;
if (auto *progressReporter = qobject_cast<IExportProgressReporting *>(pluginInstance)) {
progressReporter->setProgressCallback([](int percent) {
qDebug() << "Прогресс экспорта:" << percent << "%";
});
}
exporter->exportToFile(filePath, data);
}
Пояснения к коду
IExportPlugin — компактный обязательный интерфейс из четырёх методов, достаточный для самого базового использования любого плагина экспорта. IExportProgressReporting — отдельный, необязательный интерфейс для плагинов, способных сообщать о прогрессе долгой операции — не каждый формат экспорта действительно долгий (экспорт в простой текстовый формат может быть мгновенным), и принуждение всех плагинов реализовывать прогресс-репортинг было бы избыточным усложнением их минимальной реализации.
В exportDocument() приложение сначала получает обязательный интерфейс IExportPlugin через qobject_cast, а затем отдельно, опционально, проверяет наличие IExportProgressReporting через тот же механизм — если конкретный плагин его не реализует, qobject_cast просто вернёт nullptr, и код корректно продолжит работу без обновления прогресса, не нарушая основной функциональности экспорта.
Подводные камни
- Слишком ранняя фиксация интерфейса без запаса на будущее расширение. После того как сторонние разработчики начали писать плагины под конкретную версию интерфейса, добавление новых обязательных методов в существующий интерфейс ломает совместимость со всеми уже написанными плагинами — расширение функциональности должно идти через добавление новых опциональных дополняющих интерфейсов (как
IExportProgressReporting), а не модификацию существующего обязательного контракта. - Слишком гранулярное разделение на множество мелких интерфейсов ради теоретической гибкости, когда реальной потребности в такой гранулярности нет, усложняет как реализацию плагинов (нужно помнить, какие из десяти возможных интерфейсов реализовывать для типичного случая), так и код, использующий плагины (множество последовательных проверок
qobject_cast) — баланс между гибкостью и простотой нужно искать осознанно, ориентируясь на реальные сценарии использования, а не на абстрактную «полноту» архитектуры. - Версионирование IID интерфейса без явной стратегии при breaking changes. Суффикс
/1.0в строке IID — подсказка для будущего: при действительно breaking-изменении интерфейса (не просто добавлении опционального метода, а изменении сигнатуры существующего) стоит выпустить новый IID (/2.0) и поддерживать оба интерфейса параллельно на переходный период, чтобы старые плагины не сломались мгновенно при обновлении основного приложения. - Метод
canExport(), возвращающий упрощённый ответ без полезной диагностики для отказа. Если плагин отказывается экспортировать конкретный документ (canExport() == false), но не сообщает причину, пользователь интерфейса приложения, выбирающий формат экспорта, не понимает, почему опция недоступна — для лучшего пользовательского опыта стоит предусмотреть метод, возвращающий не просто булево значение, а причину отказа в человекочитаемом виде.