Меня всегда восхищала идея плагинов — пользовательских модулей, которые не являются частью основного приложения, но тем не менее позволяют расширять возможности приложения. Многие приложения выше определенного размера допускают определенный уровень настройки пользователями. Есть много разных подходов и много названий для него (расширения, интерфейс сценариев, модули, компоненты); Я просто скажу «плагины» с этого момента.
Самое интересное в плагинах заключается в том, что они пересекают домены приложений и языки. Вы можете найти инфраструктуру плагинов для всего, от IDE до веб-серверов и игр. Плагины могут быть разработаны на языке X, расширяя приложение, в основном на основе языка Y, для широкого спектра X и Y.
Мой план состоит в том, чтобы исследовать пространство разработки инфраструктур плагинов, рассматривая различные стратегии реализации и существующие решения в известных приложениях. Но для этого мне нужно сначала описать некоторые основные термины и понятия — общий язык , который позволит нам рассуждать о плагинах.
Пример — плагины для приложения Python
Я начну с примера с простого приложения и инфраструктуры плагинов для него. И приложение, и плагины будут написаны на Python 3.
Давайте начнем с введения задачи. Пример — небольшая, но функциональная часть некой издательской системы, скажем, движка блогов . Это та часть, которая превращает размеченный текст в HTML. Чтобы позаимствовать у reST , поддерживаемая разметка:
before markup :role:text after markup
Здесь «роль» определяет тип разметки, а «текст» — это текст, к которому применяется разметка. Типовые роли (опять же, из интерпретируемых ролей reST ): код, математика или верхний индекс [1] .
Теперь, где плагины приходят сюда? Идея состоит в том, чтобы позволить базовому приложению выполнять синтаксический анализ текста, оставляя реализацию конкретной роли плагинам. Другими словами, я хотел бы позволить авторам плагинов легко добавлять роли в приложение. В этом суть идеи плагинов: вместо жесткого кодирования функциональности приложения, пусть пользователи расширяют его. Опытные пользователи любят настраивать приложения для своих конкретных нужд и могут улучшать ваше приложение сверх ваших первоначальных намерений. С вашей точки зрения, это все равно что делать работу бесплатно — беспроигрышная ситуация.
В любом случае, существует множество способов реализации плагинов в Python [2] . Мне нравится следующий подход:
class IPluginRegistry(type):
plugins = []
def __init__(cls, name, bases, attrs):
if name != 'IPlugin':
IPluginRegistry.plugins.append(cls)
class IPlugin(object, metaclass=IPluginRegistry):
def __init__(self, post=None, db=None):
""" Initialize the plugin. Optinally provide the db.Post that is
being processed and the db.DB it belongs to.
"""
self.post = post
self.db = db
""" Plugin classes inherit from IPlugin. The methods below can be
implemented to provide services.
"""
def get_role_hook(self, role_name):
""" Return a function accepting role contents.
The function will be called with a single argument - the role
contents, and should return what the role gets replaced with.
None if the plugin doesn't provide a hook for this role.
"""
return None
Плагин — это класс, который наследуется от IPlugin. Некоторая хитрость метакласса гарантирует, что сам акт наследования от него, плагин регистрируется в системе.
Метод get_role_hook является примером ловушки . Хук — это то, что приложение предоставляет, и плагины могут быть прикреплены к нему. Прикрепляя к хуку (в нашем случае — реализуя метод get_role_hook), плагин может сообщить приложению, что он хочет участвовать в соответствующей задаче. Здесь плагин, реализующий ловушку, будет вызван приложением, чтобы выяснить, какие роли он поддерживает.
Вот пример плагина:
class TtFormatter(IPlugin):
""" Acts on the 'tt' role, placing the contents inside <tt> tags.
"""
def get_role_hook(self, role_name):
return self._tt_hook if role_name == 'tt' else None
def _tt_hook(self, contents):
return '<tt>' + contents + '</tt>'
Он реализует следующее преобразование:
text :tt:in tt tag here
чтобы:
text <tt>in tt tag</tt> here
Как видите, я решил позволить хуку вернуть функцию. Это полезно, поскольку оно может дать приложению немедленное представление о том, поддерживает ли плагин какую-либо роль (если он возвращает None, то нет). Приложение также может кэшировать функцию, возвращаемую плагинами, для более эффективного вызова позже. Есть, конечно, много вариаций на эту тему. Например, плагин может вернуть список всех ролей, которые он поддерживает.
Теперь было бы интересно посмотреть, как обнаруживаются плагины , т.е. как приложение узнает, какие плагины присутствуют в системе? Опять же, динамизм Python позволяет нам легко реализовать очень гибкую схему обнаружения:
def discover_plugins(dirs):
""" Discover the plugin classes contained in Python files, given a
list of directory names to scan. Return a list of plugin classes.
"""
for dir in dirs:
for filename in os.listdir(dir):
modname, ext = os.path.splitext(filename)
if ext == '.py':
file, path, descr = imp.find_module(modname, [dir])
if file:
# Loading the module registers the plugin in
# IPluginRegistry
mod = imp.load_module(modname, file, path, descr)
return IPluginRegistry.plugins
Эта функция может использоваться приложениями для поиска и загрузки плагинов. Он получает список каталогов, в которых нужно искать модули Python. Каждый модуль загружен, который выполняет определения класса в нем. Те классы, которые наследуются от IPlugin, регистрируются в IPluginRegistry, который затем может быть запрошен.
Вы заметите, что конструктор IPlugin принимает два необязательных аргумента — post и db. Для плагинов, которые имеют больше, чем просто самые основные возможности, приложение должно также предоставлять API для себя, что позволило бы плагинам запрашивать и манипулировать им. Это делают аргументы post и db — каждый плагин получит объект Post, представляющий сообщение в блоге, над которым он работает, а также объект DB, представляющий основную базу данных блога.
Чтобы увидеть, как они могут быть использованы плагином, давайте добавим еще один хук к IPlugin:
def get_contents_hook(self):
""" Return a function accepting full document contents.
The functin will be called with a single argument - the document
contents (after paragraph splitting and role processing), and
should return the transformed contents.
None if the plugin doesn't provide a hook for this role.
"""
return None
Этот хук позволяет плагинам регистрировать функции, которые преобразуют все содержимое поста, а не только текст, размеченный ролями [3] . Вот пример плагина, который использует его:
class Narcissist(IPlugin):
def __init__(self, post, db):
super().__init__(post, db)
self.repl = '<b>I ({0})</b>'.format(self.post.author)
def get_contents_hook(self):
return self._contents_hook
def _contents_hook(self, contents):
return re.sub(r'\bI\b', self.repl, contents)
Как следует из названия, это плагин для пользователей с нарциссическими тенденциями. Он находит все вхождения «я» в тексте, добавляет имя автора в скобках и выделяет его жирным шрифтом. Идея здесь в том, чтобы показать, как объект post, переданный плагину, можно использовать для доступа к информации из приложения. Предоставление таких деталей плагинам делает инфраструктуру чрезвычайно гибкой.
Наконец, давайте посмотрим, как приложение на самом деле использует плагины. Вот простая функция htmlize, которая получает объекты post и db, а также список плагинов. Он выполняет свое собственное преобразование содержимого публикации, заключая все абзацы в теги <p> … </ p>, а затем передает работу плагинам, сначала запуская зависимые от роли перехватчики, а затем перехватывает все содержимое [4]. ] :
RoleMatch = namedtuple('RoleMatch', 'name contents') def htmlize(post, db, plugins=[]): """ pass """ contents = post.contents # Plugins are classes - we need to instantiate them to get objects. plugins = [P(post, db) for P in plugins] # Split the contents to paragraphs paragraphs = re.split(r'\n\n+', contents) for i, p in enumerate(paragraphs): paragraphs[i] = '<p>' + p.replace('\n', ' ') + '</p>' contents = '\n\n'.join(paragraphs) # Find roles in the contents. Create a list of parts, where each # part is either text that has no roles in it, or a RoleMatch # object. pos = 0 parts = [] while True: match = ROLE_REGEX.search(contents, pos) if match is None: parts.append(contents[pos:]) break parts.append(contents[pos:match.start()]) parts.append(RoleMatch(match.group(1), match.group(2))) pos = match.end() # Ask plugins to act on roles for i, part in enumerate(parts): if isinstance(part, RoleMatch): parts[i] = _plugin_replace_role( part.name, part.contents, plugins) # Build full contents back again, and ask plugins to act on # contents. contents = ''.join(parts) for p in plugins: contents_hook = p.get_contents_hook() if contents_hook: contents = contents_hook(contents) return contents def _plugin_replace_role(name, contents, plugins): """ The first plugin that handles this role is used. """ for p in plugins: role_hook = p.get_role_hook(name) if role_hook: return role_hook(contents) # If no plugin handling this role is found, return its original form return ':{0}:{1}'.format(name, contents)
Если вы заинтересованы в коде, этот образец приложения (с простым драйвером, который обнаруживает плагины с помощью вызова explore_plugins и вызова htmlize) можно скачать здесь .
Основные концепции плагинов
Прочитав о плагинах и изучив код многих приложений, мне стало ясно, что для описания определенной инфраструктуры плагинов вам действительно необходимо рассмотреть только 4 фундаментальных аспекта / понятия [5] :
- открытие
- Регистрация
- Хуки приложений, к которым подключаются плагины (так называемые «точки монтирования»)
- Предоставление возможностей приложения обратно плагинам (иначе расширение API)
Между ними есть некоторые области дублирования (например, иногда трудно отличить обнаружение от регистрации), но я считаю, что вместе они охватывают более 95% того, что нужно понять при изучении инфраструктуры плагинов для конкретного приложения.
открытие
Это механизм, с помощью которого работающее приложение может выяснить, какие плагины оно имеет в своем распоряжении. Чтобы «обнаружить» плагин, нужно искать в определенных местах, а также знать, что искать. В нашем примере, функция Discover_plugins реализует это — плагины — это классы Python, которые наследуются от известного базового класса, содержащегося в модулях, расположенных в известных местах.
Регистрация
Это механизм, с помощью которого плагин сообщает приложению: «Я здесь, готов к работе». По общему признанию, регистрация обычно имеет большое совпадение с обнаружением, но я все еще хочу разделить две концепции, поскольку это делает вещи более явными (не во всех языках регистрация такая же автоматическая, как показывает наш пример).
Крючки
Крючки также называются «точками крепления» или «точками расширения». Это места, где плагин может «присоединиться» к приложению, сигнализируя о том, что он хочет знать об определенных событиях и участвовать в потоке. Точная природа крючков очень сильно зависит от применения. В нашем примере хуки позволяют подключаемым модулям вмешиваться в процесс преобразования текста в HTML, выполняемый приложением. В примере также демонстрируются как грубозернистые крючки (обработка всего содержимого), так и мелкозернистые крючки (обработка только определенных размеченных кусков).
Предоставление API приложения плагинам
Чтобы сделать плагины действительно мощными и универсальными, приложению необходимо предоставить им доступ к себе посредством предоставления API, который могут использовать плагины. В нашем примере API-интерфейс относительно прост — приложение просто передает некоторые из своих внутренних объектов плагинам. API имеют тенденцию становиться намного более сложными, когда задействованы несколько языков. Я надеюсь показать некоторые интересные примеры в будущих статьях.
Изучение некоторых известных приложений
Теперь, когда у нас есть четко определенные концепции, я хочу закончить эту статью, исследуя инфраструктуры плагинов нескольких очень распространенных приложений. Оба написаны на языках высокого уровня, что делает инфраструктуру относительно простой. Я буду представлять более сложные инфраструктуры в будущих статьях, как только рассмотрю технические детали реализации плагинов в C или C ++.
ртутный
Mercurial (Hg) — это популярная VCS (система контроля версий), написанная на Python. Mercurial хорошо известен своей расширяемостью — большая часть его функциональности обеспечивается расширениями Python . Некоторые расширения стали достаточно популярными, чтобы распространяться вместе с основным приложением, а некоторые необходимо загружать отдельно.
Обнаружение: расширения, которые пользователь хочет загрузить, должны быть явно указаны в разделе [extensions] файла конфигурации Mercurial (.hgrc).
Регистрация: расширения — это модули Python, которые экспортируют определенные функции (например, uisetup) и значения (например, cmdtable), которые ищет Mercurial. Существование любой такой функции или значения равнозначно регистрации расширения в Mercurial.
Хуки: функции верхнего уровня, такие как uisetup и extsetup, служат в качестве грубых хуков. Более мелкие хуки могут быть явно зарегистрированы путем вызова, например, ui.setconfig (‘hooks’, …) для объекта пользовательского интерфейса, переданного в обратные вызовы uisetup и команды.
API приложения. Объекты приложения Mercurial, такие как пользовательский интерфейс и репо, передаваемые в хуки, предоставляют возможность запрашивать приложение и действовать от его имени.
WordPress
WordPress — самый популярный блог-движок в Интернете и, возможно, самая популярная система управления контентом в целом. Он написан на PHP, и его обширная система плагинов (плагины также написаны на PHP), возможно, является его наиболее важной особенностью.
Обнаружение: плагины должны быть .php файлами (или каталогами с такими файлами), помещенными в специальный каталог wp-content / plugins . Они должны содержать специальный комментарий с метаданными вверху, который WordPress использует для распознавания их как допустимых плагинов.
Регистрация и хуки : плагины регистрируют себя, добавляя хуки через специальные вызовы API. Крючки бывают двух видов — фильтры и действия. Фильтры очень похожи на плагины, показанные в нашем примере (преобразуйте текст в окончательный вид). Действия являются более общими и позволяют подключать плагины ко многим различным операциям, выполняемым WordPress.
API приложения: WordPress предоставляет свои плагины довольно прямо. Основные объекты приложения (такие как $ wpdb) просто доступны как глобальные переменные для использования плагинами.
Вывод
Основной целью этой статьи было определить общий язык для рассуждения о плагинах. Четыре концепции должны предоставить один инструмент для изучения и изучения инфраструктуры плагинов данного приложения: 1) как обнаруживаются плагины, 2) как они регистрируются в приложении, 3) какие хуки могут использовать плагины для расширения приложения и 4) какой API приложение предоставляет плагинам.
Примеры, представленные здесь, были в основном о приложениях Python с плагинами Python (примером WordPress является PHP, который находится примерно на том же уровне выразительности, что и Python). Плагины для статических языков, и в особенности кросс-языковые плагины, создают больше проблем при реализации. В будущих статьях я собираюсь изучить некоторые стратегии реализации плагинов в C, C ++ и смешанных статически-динамических языках, а также изучить инфраструктуры плагинов в некоторых известных приложениях.
[1] | Упрощенная разметка, такая как окружающий текст звездочками (т.е. * курсив *), может поддерживаться аналогичным образом, но я хотел сосредоточиться здесь на плагинах, а не на разборе текста. |
[2] | И любой другой язык, для этой цели. Вероятно, поэтому существует очень мало хорошо известных платформ плагинов (даже в низкоуровневых языках, таких как C или C ++). Слишком легко (и заманчиво) накатывать свои собственные. |
[3] | Естественно, здесь есть компромисс. С одной стороны, этот хук позволяет очень сложные преобразования с помощью плагинов. С другой стороны, приложение не дает многим плагину — каждый плагин должен анализировать содержимое сам. Сравните это с get_role_hook, где приложение выполняет синтаксический анализ и передает плагину только роль и его содержимое. |
[4] | Обратите внимание, что это не делает попытку быть эффективной. Например, нет никакого смысла спрашивать плагины о ролях, которые они знают каждый раз — эта информация может быть кэширована. |
[5] | Поскольку последующее обсуждение несколько абстрактно, я сознательно начал статью с примера. Он должен обеспечить реальную основу для связи понятий. |