メインコンテンツまでスキップ

プラグイン機構

このドキュメントの目的

コード変更なしで機能を追加できる本格的なプラグイン機構の仕組みを理解することが目標です。


目次

  1. プラグイン機構とは
  2. 言語による実装の違い
  3. プラグインの発見
  4. プラグインの登録
  5. プラグインの有効化・無効化
  6. プラグインのライフサイクル
  7. プラグイン機構の注意点
  8. まとめ

プラグイン機構とは

依存の組み立てとの違い

┌─────────────────────────────────────────────┐
│ 依存の組み立て(前のドキュメント) │
├─────────────────────────────────────────────┤
│ │
│ ・起動時にコードで組み立てる │
│ ・どの実装を使うかはコードに書いてある │
│ ・切り替えにはコード変更が必要 │
│ │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│ プラグイン機構(このドキュメント) │
├─────────────────────────────────────────────┤
│ │
│ ・プラグインを自動で発見する │
│ ・ファイル追加だけで機能が増える │
│ ・設定でON/OFFを切り替えられる │
│ ・コード変更なしで拡張できる │
│ │
└─────────────────────────────────────────────┘

プラグイン機構の構成要素

┌─────────────────────────────────────────────┐
│ プラグイン機構に必要なもの │
├─────────────────────────────────────────────┤
│ │
│ 1. プラグインインターフェース │
│ └── プラグインが実装すべき契約 │
│ │
│ 2. 発見メカニズム │
│ └── プラグインを見つける仕組み │
│ │
│ 3. 登録メカニズム │
│ └── 見つけたプラグインを使えるようにする │
│ │
│ 4. 有効化・無効化 │
│ └── プラグインのON/OFF制御 │
│ │
│ 5. ライフサイクル管理 │
│ └── 初期化・実行・終了の制御 │
│ │
└─────────────────────────────────────────────┘

言語による実装の違い

考え方は同じ、実装は異なる

┌─────────────────────────────────────────────┐
│ どの言語でも共通の考え方 │
├─────────────────────────────────────────────┤
│ │
│ ・プラグインを発見する仕組みが必要 │
│ ・インターフェースで契約を定義 │
│ ・実行時に動的に読み込む │
│ │
│ ただし、実現方法は言語の特性に依存 │
│ │
└─────────────────────────────────────────────┘

言語ごとの発見メカニズム

言語標準的な仕組み特徴
JavaServiceLoader (SPI)META-INF/servicesにクラス名を記述
PythonEntry Pointssetup.pyやpyproject.tomlで宣言
Node.jspackage.jsonnpmパッケージとして配布
C#/.NETMEF属性でエクスポート/インポートを宣言
GoPlugin 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(): │
│ └── プラグイン削除時 │
│ │
└─────────────────────────────────────────────┘

依存の組み立てとの使い分け

┌─────────────────────────────────────────────┐
│ いつどちらを使うか │
├─────────────────────────────────────────────┤
│ │
│ 依存の組み立て: │
│ ├── 実装が固定的 │
│ ├── コンパイル時に決まる │
│ ├── シンプルで十分な場合 │
│ └── アプリケーション内部の構造 │
│ │
│ プラグイン機構: │
│ ├── 実装を動的に追加したい │
│ ├── 設定で切り替えたい │
│ ├── 第三者が拡張できるようにしたい │
│ └── 製品としての拡張ポイント │
│ │
└─────────────────────────────────────────────┘

プラグイン機構の注意点

セキュリティリスク

┌─────────────────────────────────────────────┐
│ プラグインは「信頼されていないコード」 │
├─────────────────────────────────────────────┤
│ │
│ 悪意のあるプラグイン: │
│ ├── 任意のコードを実行できる │
│ ├── 機密データにアクセスできる │
│ └── システムを乗っ取れる │
│ │
│ サードパーティ製プラグイン: │
│ ├── 脆弱性を含んでいる可能性 │
│ ├── 依存ライブラリの脆弱性 │
│ └── セキュリティ更新が遅れる │
│ │
└─────────────────────────────────────────────┘

安定性リスク

┌─────────────────────────────────────────────┐
│ プラグインがシステム全体に影響する │
├─────────────────────────────────────────────┤
│ │
│ バグのあるプラグイン: │
│ ├── システム全体をクラッシュさせる │
│ ├── メモリリークを引き起こす │
│ └── パフォーマンスを低下させる │
│ │
│ プラグイン間の問題: │
│ ├── 競合による予期しない動作 │
│ ├── 依存ライブラリのバージョン衝突 │
│ └── 初期化順序の問題 │
│ │
└─────────────────────────────────────────────┘

保守リスク

┌─────────────────────────────────────────────┐
│ 長期的な維持の難しさ │
├─────────────────────────────────────────────┤
│ │
│ 互換性の問題: │
│ ├── コア更新時にプラグインが動かなくなる │
│ ├── プラグインAPI変更の影響範囲が大きい │
│ └── 後方互換性の維持コスト │
│ │
│ 放棄されたプラグイン: │
│ ├── メンテナンスされなくなる │
│ ├── セキュリティ修正が行われない │
│ └── 新バージョンで動作しない │
│ │
└─────────────────────────────────────────────┘

対策

┌─────────────────────────────────────────────┐
│ リスクを軽減するために │
├─────────────────────────────────────────────┤
│ │
│ セキュリティ: │
│ ├── プラグインの署名・検証 │
│ ├── サンドボックス環境での実行 │
│ ├── 権限の最小化(必要な機能だけ許可) │
│ └── 信頼できるソースからのみインストール │
│ │
│ 安定性: │
│ ├── プラグインの分離(別プロセス/コンテナ) │
│ ├── タイムアウトとリソース制限 │
│ ├── 障害時の自動無効化 │
│ └── 十分なテストとレビュー │
│ │
│ 保守性: │
│ ├── 安定したプラグインAPIの設計 │
│ ├── バージョニングと非推奨プロセス │
│ ├── プラグインの互換性テスト │
│ └── 公式プラグインの提供 │
│ │
└─────────────────────────────────────────────┘

プラグイン機構を採用すべきか

┌─────────────────────────────────────────────┐
│ 判断基準 │
├─────────────────────────────────────────────┤
│ │
│ 採用すべき場合: │
│ ├── 拡張性がビジネス上の要件 │
│ ├── サードパーティによる拡張が価値を生む │
│ └── リスク管理のコストを払える │
│ │
│ 避けるべき場合: │
│ ├── 内部でのみ使うシステム │
│ ├── セキュリティ要件が非常に厳しい │
│ └── 保守リソースが限られている │
│ │
│ 「依存の組み立て」で十分なことも多い │
│ │
└─────────────────────────────────────────────┘

まとめ

プラグイン機構の要素

┌─────────────────────────────────────────────┐
│ プラグイン機構に必要なもの │
├─────────────────────────────────────────────┤
│ │
│ 1. プラグインインターフェース │
│ → プラグインが満たすべき契約 │
│ │
│ 2. 発見メカニズム │
│ → ファイル追加だけで認識される │
│ │
│ 3. レジストリ │
│ → プラグインを管理する中央の場所 │
│ │
│ 4. 有効化・無効化 │
│ → 設定でON/OFFを制御 │
│ │
│ 5. ライフサイクル管理 │
│ → 初期化・実行・終了の制御 │
│ │
└─────────────────────────────────────────────┘

クリーンアーキテクチャとの関係

プラグイン機構は依存ルールの延長線上にある

依存ルール:
「コアは外側を知らない」

プラグイン機構:
「コアはプラグインの存在すら知らなくていい」
「ファイル追加だけで機能が増える」