プラグイン機構
このドキュメントの目的
コード変更なしで機能を追加できる本格的なプラグイン機構の仕組みを理解することが目標です。
目次
プラグイン機構とは
依存の組み立てとの違い
┌─────────────────────────────────────────────┐
│ 依存の組み立て(前のドキュメント) │
├─────────────────────────────────────────────┤
│ │
│ ・起動時にコードで組み立てる │
│ ・どの実装を使うかはコードに書いてある │
│ ・切り替えにはコード変更が必要 │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ プラグイン機構(このドキュメント) │
├─────────────────────────────────────────────┤
│ │
│ ・プラグインを自動で発見する │
│ ・ファイル追加だけで機能が増える │
│ ・設定でON/OFFを切り替えられる │
│ ・コード変更なしで拡張できる │
│ │
└─────────────────────────────────────────────┘
プラグイン機構の構成要素
┌─────────────────────────────────────────────┐
│ プラグイン機構に必要なもの │
├─────────────────────────────────────────────┤
│ │
│ 1. プラグインインターフェース │
│ └── プラグインが実装すべき契 約 │
│ │
│ 2. 発見メカニズム │
│ └── プラグインを見つける仕組み │
│ │
│ 3. 登録メカニズム │
│ └── 見つけたプラグインを使えるようにする │
│ │
│ 4. 有効化・無効化 │
│ └── プラグインのON/OFF制御 │
│ │
│ 5. ライフサイクル管理 │
│ └── 初期化・実行・終了の制御 │
│ │
└─────────────────────────────────────────────┘
言語による実装の違い
考え方は同じ、実装は異なる
┌─────────────────────────────────────────────┐
│ どの言語でも共通の考え方 │
├─────────────────────────────────────────────┤
│ │
│ ・プラグインを発見する仕組みが必要 │
│ ・インターフェースで契約を定義 │
│ ・実行時に動的に読み込む │
│ │
│ ただし、実現方法は言語の特性に依存 │
│ │
└─────────────────────────────────────────────┘
言語ごとの発見メカニズム
| 言語 | 標準的な仕組み | 特徴 |
|---|---|---|
| Java | ServiceLoader (SPI) | META-INF/servicesにクラス名を記述 |
| Python | Entry Points | setup.pyやpyproject.tomlで宣言 |
| Node.js | package.json | npmパッケージとして配布 |
| C#/.NET | MEF | 属性でエクスポート/インポートを宣言 |
| Go | Plugin package | 共有ライブラリとして読み込み(制限あり) |
言語特性による違い
┌──────────────────────── ─────────────────────┐
│ 動的型付け言語(Python, JavaScript) │
├─────────────────────────────────────────────┤
│ │
│ ・型チェックが緩い │
│ ・実行時に柔軟に読み込める │
│ ・インターフェースの強制が難しい │
│ ・ダックタイピングで対応することも │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 静的型付け言語(Java, C#, Go) │
├─────────────────────────────────────────────┤
│ │
│ ・インターフェースで型安全に定義 │
│ ・コンパイル時にある程度チェック可能 │
│ ・動的読み込みには言語サポートが必要 │
│ ・リフレクションを使うことが多い │
│ │
└─────────────────────────────────────────────┘
以降の例はJavaで説明しますが、考え方は他の言語でも同様です。
プラグインの発見
なぜ発見が必要か
┌─────────────────────────────────────────────┐
│ 発見メカニズムがない場合 │
├─────────────────────────────────────────────┤
│ │
│ 新しいプラグインを追加するたびに: │
│ ├── コードを変更する │
│ ├── 登録処理を追加する │
│ └── 再コンパイルする │
│ │
│ これでは「プラグイン」とは言えない │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 発見メカニズムがある場合 │
├─────────────────────────────────────────────┤
│ │
│ 新しいプラグインを追加するには: │
│ └── ファイルを所定の場所に置くだけ │
│ │
│ システムが自動で見つけて使えるようにする │
│ │
└─────────────────────────────────────────────┘
発見の方法
方法1: ディレクトリスキャン
plugins/
├── email-sender.jar
├── slack-sender.jar
└── line-sender.jar
起動時に plugins/ ディレクトリをスキャン
→ 見つけたファイルをプラグインとして読み込む
方法2: 設定ファイルで列挙
plugins.yml:
enabled:
- email-sender
- slack-sender
設定ファイルに書かれたプラグインを読み込む
→ 明示的で、有効/無効も管理しやすい
方法3: サービスローダー(Java/SPIなど)
META-INF/services/NotificationSender:
com.example.EmailSender
com.example.SlackSender
標準化された仕組みでプラグインを宣言
→ 言語やフレームワークのサポートを活用
プラグインの登録
プラグインインターフェース
┌─────────────────────────────────────────────┐
│ プラグインが実装すべきインターフェース │
├─────────────────────────────────────────────┤
│ │
│ Plugin(プラグイン共通) │
│ ├── getName(): プラグイン名 │
│ ├── getVersion(): バージョン │
│ ├── initialize(): 初期化処理 │
│ └── shutdown(): 終了処理 │
│ │
│ NotificationSender(機能固有) │
│ └── send(message): 通知送信 │
│ │
└─────────────────────────────────────────────┘
プラグインレジストリ
┌─────────────────────────────────────────────┐
│ レジストリの役割 │
├─────────────────────────────────────────────┤
│ │
│ 発見したプラグインを管理する中央の場所 │
│ │
│ 機能: │
│ ├── プラグインの登録 │
│ ├── プラグインの検索 │
│ ├── 有効なプラグインの一覧取得 │
│ └── プラグインのライフサイクル管理 │
│ │
│ コアはレジストリを通じてプラグインを使う │
│ │
└────────── ───────────────────────────────────┘
登録の流れ
1. 起動時にプラグインを発見
└── plugins/ をスキャン
2. 各プラグインをレジストリに登録
└── registry.register(plugin)
3. プラグインの初期化
└── plugin.initialize()
4. コアがレジストリから取得して使用
└── senders = registry.getAll(NotificationSender)
プラグインの有効化・無効化
なぜ必要か
┌─────────────────────────────────────────────┐
│ 有効化・無効化が必要な場面 │
├─────────────────────────────────────────────┤
│ │
│ ・特定の環境でのみ使いたい │
│ 本番: Email + Slack │
│ 開発: コンソール出力のみ │
│ │
│ ・問題が起きたプラグインを一時停止 │
│ │
│ ・ライセンスに応じた機能制限 │
│ │
└─────────────────────────────────────────────┘
設定による制御
plugins.yml:
notification:
email:
enabled: true
config:
smtp_host: mail.example.com
slack:
enabled: false
line:
enabled: true
config:
channel_token: xxx
設定ファイルで:
├── どのプラグインを有効にするか
└── プラグイン固有の設定
レジストリでの管理
┌─────────────────────────────────────────────┐
│ 有効化・無効化の仕組み │
├─────────────────────────────────────────────┤
│ │
│ registry.enable("email-sender") │
│ registry.disable("slack-sender") │
│ │
│ senders = registry.getEnabled(Sender) │
│ // 有効なプラグインのみ返る │
│ │
│ コアは有効なプラグインだけを使う │
│ 無効なプラグインの存在を知る必要がない │
│ │
└─────────────────────────────────────────────┘
プラグインのライフサイクル
ライフサイクルの段階
┌─────────────────────────────────────────────┐
│ プラグインの状態遷移 │
├─────────────────────────────────────────────┤
│ │
│ 発見 → 登録 → 初期化 → 実行中 → 終了 │
│ │
│ 発見: │
│ └── ファイルとして存在を認識 │
│ │
│ 登録: │
│ └── レジストリに追加 │
│ │
│ 初期化: │
│ └── リソース確保、接続確立 │
│ │
│ 実行中: │
│ └── 機能を提供 │
│ │
│ 終了: │
│ └── リソース解放、接続切断 │
│ │
└─────────────────────────────────────────────┘
ライフサイクルフック
┌─────────────────────────────────────────────┐
│ プラグインが実装するフック │
├─────────────────────────────────────────────┤
│ │
│ onLoad(): │
│ └── プラグイン読み込み時 │
│ │
│ onEnable(): │
│ └── 有効化された時 │
│ │
│ onDisable(): │
│ └── 無効化された時 │
│ │
│ onUnload(): │
│ └── プラグイン削除時 │
│ │
└─────────────────────────────────────────────┘
依存の組み立てとの使い分け
┌─────────────────────────────────────────────┐
│ いつどちらを使うか │
├─────────────────────────────────────────────┤
│ │
│ 依存の組み立て: │
│ ├── 実装が固定的 │
│ ├── コンパイル時に決まる │
│ ├── シンプルで十分な場合 │
│ └── アプリケーション内部の構造 │
│ │
│ プラグイン機構: │
│ ├── 実装を動的に追加したい │
│ ├── 設定で切り替えたい │
│ ├── 第三者が拡張できるようにしたい │
│ └── 製品としての拡張ポイント │
│ │
└─────────────────────────────────────────────┘
プラグイン機構の注意点
セキュリティリスク
┌─────────────────────────────────────────────┐
│ プラグインは「信頼されていないコード」 │
├─────────────────────────────────────────────┤
│ │
│ 悪意のあるプラグイン: │
│ ├── 任意のコードを実行できる │
│ ├── 機密データにアクセスできる │
│ └── システムを乗っ取れる │
│ │
│ サードパーティ製プラグイン: │
│ ├── 脆弱性を含んでいる可能性 │
│ ├── 依存ライブラリの脆弱性 │
│ └── セキュリティ更新が遅れる │
│ │
└─────────────────────────────────────────────┘