Разработка ПО - системы плагинов

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

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


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

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

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

Общие понятия

Системы плагинов бывают разных типов и форм. Разобраться в этом и проиллюстрировать их структуру помогут следующие основные концепции:

Концептуальные компоненты базовой системы плагинов

Программа

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

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

Хуки

В программе, поведение которой вы пытаетесь изменить, что-то должно запускать код, о котором она ничего не знает. В противном случае непонятно, каким образом можно изменить ее поведение.

Для запуска кода плагина используется хук. Реальными примерами могут служить обратные вызовы модели Rails или хуки жизненного цикла компонентов Vue.

Вот пример простого хука:

class RecordSaver
def save
writeToDisk()
onSaveHook()
end
def onSaveHook()
end
private
# ...разные функции
end

Видно, что у onSaveHook нет никакого поведения. В этом примере ожидается, что это поведение будет определено классом, расширяющим Record Saver и реализующим новое поведение в подклассе.

Плагины

Плагин — это код, который программисты пишут и “подключают” (plug in) для расширения возможностей программы.

Ниже представлена возможность расширения класса Record Saver из примера выше:

class LoggedRecordSaver < RecordSaver
def onSaveHook()
puts 'Saving record.'
end
end

Теперь после вызова Logged Record Saver#save() будет выводиться Saving record. после вызова #writeToDisk(). Это базовый вариант совершенствования функциональных свойств программ путем расширения RecordSaver.

Также следует отметить, что RecordSaver ничего не знает о LoggedRecordSaver, т. е. это однонаправленное взаимодействие.

Загрузчик

Плагин будет бесполезен, если его нельзя запустить. Но чтобы началось исполнение кода плагина, его нужно загрузить. Для этого есть несколько методов. Вот два из них:

  • Plugin-driven — управляемый плагином саморегистрируемый доступ к программе.
  • Program-driven — управляемый программой поиск и загрузка плагина.

В первом случае программа предоставляет метод для саморегистрации плагина в программе. Во втором случае программа находит плагин, например, загружая все файлы в папку с именем *_plugin или загружая манифест.


Создание чат-бота с плагинами

Чтобы можно было поболтать с чат-ботом Online или получать от него голосовые напоминания пришлось потратить на его создание свои выходные.

В чат-бот OneLine использованы плагины

Суть задачи проста: получить сообщение, затем произвольно преобразовать и отправить ответ. При этом была создана базовая система плагинов, которая реализует упомянутые выше концепции.

Загрузчик

Программная часть OneLine включает подмножество статических методов в модуле Core::Plugin, определяющее реализацию:

  • Загрузить plugin (#load);
  • Отслеживать все загруженные plugins (@@plugins);
  • Вызвать все загруженные plugins (#call_all).
module Core
module Plugin
@@plugins = {}
def self.plugins
return @@plugins
end
def load(plugin)
Core::Plugin.plugins[plugin.class] = plugin
end
def self.call_all(data, params = {})
plugin_responses = []
Core::Plugin.plugins.each { |key, plugin|
plugin_response = plugin.call(data, params)
plugin_responses << plugin_response if plugin_response
}
end
end

Обратите внимание, в этом примере загрузчик и хук объединены.

Загрузчик — это функция load, которую реализации Plugin могут вызывать для осведомления программы об этом.

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

Интерфейс плагина

Определяет ожидаемое поведение для всех реализаций plugins. В этом случае:

  • Проверка необходимости запуска плагина (#process?).
  • Метод, который будет вызываться для запуска плагина (#process).
  • Возвращаемый плагином стандартный ответ (#to_response).
module Core
module Plugin
def process?(data, params = {})
return false
end
def process(data, params = {})
end
def to_response(result)
return Core::PluginResponse.new(result)
end
end
end

Функция #process — вызывается хуком (при получении сообщения).

Реализации плагинов

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

  • Расскажи мне анекдот (lib/oneline/jokes).
  • Следи за моим списком дел (lib/oneline/scheduler).
  • и многое другое!

Самый простой пример — это плагин, который сообщает текущее время.

module When
# Not called "Time" to prevent conflicts.
class Plugin
include Core::Plugin
def initialize(tasks = {})
load(self)
end
def process(text, params = {})
return {messages: ["It is #{Time.current.strftime('%b %e, %Y - %l:%M%P %Z')}."]}
end
def process?(text, params = {})
return text.downcase === 'time' if text.present?
end
end
::When::Plugin.new # Self-instantiates and registers the plugin
end

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

При том, что системы плагинов подходят не для всех сценариев, в некоторых случаях они могут быть весьма полезными.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Joseph Gefroh: How to Design Software — Plugin Systems

Предыдущая статьяРазличия между псевдонимами типов и интерфейсами в TypeScript 4.6
Следующая статьяКак продвигаться в роли разработчика?