RubyにおけるPluginの設計技法について、実際に多くのユーザーに利用されている
3つの gem を参考にしてみます。
1. Minitest
Minitest とは?
Minitest は UnitTest を行うためのライブラリです。
Plugin機構のルール
- ファイルは minitest/XXX_plugin.rb で作成する
- RubyのLOAD_PATHに配置する(rubygemsやその他)
- plugin_XXX_init メソッドを定義すると初期化処理として実行される
- plugin_XXX_options メソッドを定義するとオプションパースの処理として実行される
Plugin機構のコード
- Minitest の Plugin 関連のコード
minitest/lib/minitest.rb L77-202 - seattlerb/minitest - GitHub
# 略 module Minitest # 略 # 各Pluginの初期化 def self.init_plugins options # :nodoc: self.extensions.each do |name| msg = "plugin_#{name}_init" send msg, options if self.respond_to? msg end end # Plugin の読み込み def self.load_plugins # :nodoc: return unless self.extensions.empty? seen = {} require "rubygems" unless defined? Gem Gem.find_files("minitest/*_plugin.rb").each do |plugin_path| name = File.basename plugin_path, "_plugin.rb" next if seen[name] seen[name] = true require plugin_path self.extensions << name end end # 略 # 各Pluginのoption設定 unless extensions.empty? opts.separator "" opts.separator "Known extensions: #{extensions.join(", ")}" extensions.each do |meth| msg = "plugin_#{meth}_options" send msg, opts, options if self.respond_to?(msg) end end # 略 end
2. Pry
Pryとは?
Pry は Ruby の REPL ツールです。同分野の irb の上位版。
Plugin機構のルール
- gem 名が
pry-
ではじまること - lib 配下に gem 名と同じ名前の
.rb
ファイルが存在すること - customization, hooks, and command system などを任意のAPIを利用して拡張する
- Config の場合
- Pry.config を利用する
- Hook の場合
- Pry.hooks.add_hook を利用する
- Command の場合
- Pry::CommandSet.block_command, Pry::Commands.block_command, Pry::Commands.create_command 等を利用する
- Config の場合
Plugin機構のコード
Plugins - Wiki - pry/pry - GitHub
- 読み込み処理
pry/lib/pry/plugins.rb L80-88 - pry/pry - GitHub
gem のリストの中から「pry-」に一致するgemならばPluginとみなして、
Pluginのインスタンスを生成し、 @plugins
配列に追加している。
# Find all installed Pry plugins and store them in an internal array. def locate_plugins gem_list.each do |gem| next if gem.name !~ PRY_PLUGIN_PREFIX plugin_name = gem.name.split('-', 2).last plugin = Plugin.new(plugin_name, gem.name, gem, false) @plugins << plugin.tap(&:enable!) if plugin.supported? && !plugin_located?(plugin) end @plugins end
- 呼び出し処理
pry/lib/pry/command_set.rb L78-84 - pry/pry - GitHub
block_command API から追加された処理を @command
Hash に追加している
def block_command(match, description="No description.", options={}, &block) description, options = ["No description.", description] if description.is_a?(Hash) options = Pry::Command.default_options(match).merge!(options) @commands[match] = Pry::BlockCommand.subclass(match, description, options, helper_module, &block) end alias_method :command, :block_command
3. Ruboty
Rubotyとは?
Ruboty は Ruby製のチャットボットフレームワーク です
Plugin機構のルール
- gem 名が
ruboty-
ではじまること - Handler, Adapter, and Brain などを任意のクラスを継承して拡張する
- Handler の場合
- Ruboty::Handlers::Base を継承し、
on
クラスメソッドと任意の名前のインスタンスメソッドを実装する
- Ruboty::Handlers::Base を継承し、
- Adapter の場合
- Ruboty::Adapters::Base を継承し、
say
/run
等を実装する
- Ruboty::Adapters::Base を継承し、
- Handler の場合
Plugin機構のコード
- 読み込み処理1
ruboty/lib/ruboty/handlers/base.rb L7-9 - r7kamura/ruboty - GitHub
Ruboty::Handlers::Base の継承時に Ruboty.handlers に継承したクラスを追加しています
def inherited(child) Ruboty.handlers << child end
- 読み込み処理2
ruboty/lib/ruboty/robot.rb L80-83 - r7kamura/ruboty - GitHub
Ruboty の起動処理中に Handlers に追加された Plugin を初期化しています
def handlers Ruboty.handlers.map { |handler_class| handler_class.new(self) } end memoize :handlers
- 呼び出し処理
ruboty/lib/ruboty/robot.rb L25-32 - r7kamura/ruboty - GitHub
メッセージ受信時に Ruboty.handlers から対応コマンドがあるかどうか検索し、
対象であれば handler plugin を呼び出します。
def receive(attributes) message = Message.new(attributes.merge(robot: self)) unless handlers.inject(false) { |matched, handler| matched | handler.call(message) } handlers.each do |handler| handler.call(message, missing: true) end end end
まとめ
全体として必要となるのは
- plugin の名前を規約で決定する
- 起動時に命名規約に基づいて Plugin を取得し、Pluginを保持するための配列やHashに追加する
- 呼び出し時に保存しておいたPluginを取得し、処理を呼び出す
ことです。