「ダイナミックパイプライン」というメソッドチェインパターンを考案してみた
はじめに
最近、趣味でお絵かきアプリの「手ぶれ補正」機能を実装していました。複数のフィルタを組み合わせて、ユーザー設定で動的に切り替えられるようにしたい。そんな要件を満たす設計を考えているうちに、「これ、既存のパターンにぴったり当てはまるものがないな」と気づきました。
Builderパターンとも違う、Decoratorパターンとも違う、Middlewareパターンとも違う。調べてみても名前が見つからなかったので、自分で「ダイナミックパイプライン(Dynamic Pipeline)」と名付けてみました。
まだ拙い考えや設計かもしれませんが、同じような課題に直面している方の参考になれば幸いです。既存のパターン名や類似のアプローチをご存知の方がいらっしゃれば、ご教示いただけると助かります。
ダイナミックパイプラインとは
処理を「足したり引いたり」できるメソッドチェインパターンです。
// 処理を「加算」していく
const processor = new Processor()
.addFilterA(param1) // +処理A
.addFilterB(param2) // +処理B
.addFilterC(param3); // +処理C
// 後からパラメータを「更新」
processor.updateFilterA(newParam);
// 処理を「減算」(削除)
processor.removeFilter('B');
特徴
- 加算(add): 処理を後から追加できる
- 減算(remove): 追加した処理を削除できる
- 更新(update): 処理のパラメータを後から変更できる
- 順序: 追加した順番が処理の実行順序になる
-
即使用:
.build()不要、いつでも使える状態
「人の成長」で考えると
このパターンは「人が経験を積んで成長する」というメタファーでも説明できます。
const person = new Person()
.addEducation('大学') // +教育を受けた
.addSkill('プログラミング') // +スキルを習得
.addExperience('海外生活') // +経験を積んだ
.addTrauma('大きな失敗'); // +傷も人格の一部に
人が成長するように、能力や経験を「足していく」。そして状況に応じて能力を発揮したり抑えたりできる。
// スキルを忘れる(減算)
person.removeSkill('プログラミング');
// 10年後、スキルがレベルアップ(更新)
person.updateSkill('プログラミング', { level: 'expert' });
// 大切なものを失う(減算)
person.removeExperience('海外生活');
従来のパターンとの違い
メソッドチェインを使ったパターンは多くありますが、それぞれ特徴が異なります。
| パターン | 操作 | 順序 | 実行時変更 |
|---|---|---|---|
| Builder | 設定 | 関係なし | 不可(build後) |
| Decorator | 包む | 関係あり | 困難 |
| Middleware | 登録 | 関係あり | 限定的 |
| RxJS pipe | 変換 | 関係あり | 不可 |
| Chain of Responsibility | 連鎖 | 関係あり | 限定的 |
| Dynamic Pipeline | add/remove/update | 関係あり | 容易 |
ダイナミックパイプラインは「パイプライン構築と動的変更を両立」している点が特徴です。
私の実装例:手ぶれ補正
私がこのパターンに行き着いたきっかけです。手ぶれ補正はかなりニッチな話題なので、馴染みがなければ読み飛ばしていただいて構いません。パターン自体はこの分野に限定されるものではありません。
私の実装では、畳み込みベースのフィルタ(ガウシアン平滑化やカルマンフィルタなど)を生のポインタ入力に適用しています。そして:
- ユーザー設定で調整したい: 補正の強さをスライダーで変える
- 状況に応じて切り替えたい: 高速描画時は重いフィルタを無効化
- 順序が重要: ノイズ除去 → 予測補正 → 平滑化の順で処理
const pointer = new StabilizedPointer()
.addFilter('noise', 1.5)
.addFilter('kalman', 0.1)
.addFilter('gaussian', 5);
// UIで設定を変更
settingsPanel.onChange((settings) => {
pointer.updateFilter('noise', settings.noiseThreshold);
if (!settings.useSmoothing) {
pointer.removeFilter('gaussian');
}
});
// ペンの速度に応じて動的に調整
pointer.onVelocityChange((velocity) => {
if (velocity > 500) {
// 高速描画時は重いフィルタを削除(パフォーマンス優先)
pointer.removeFilter('gaussian');
} else {
pointer.addFilter('gaussian', 5);
}
});
実装のポイント(案)
1. 配列によるパイプライン管理
シンプルな配列でパイプラインを管理するのが、追加順 = 実行順を保証する素直なアプローチかもしれません。
2. 型またはユニークIDによる識別
処理ステップは、シンプルなケースでは型(例:'validation'、'transform')で識別できます。同じ型のステップを複数持つ必要がある場合は、ユニークIDで管理する方がより堅牢かもしれません。
3. パイプラインを単一関数にキャッシュ
高頻度で実行される場合、設定が変更されるたびにパイプラインを単一の関数にキャッシュすると、実行時のオーバーヘッドを最小化できるかもしれません。
まとめ
「ダイナミックパイプライン(Dynamic Pipeline)」は、処理を「足したり引いたり」できるメソッドチェインパターンです。
正式な意味での「パターン」と呼べるかどうかはわかりません。アーキテクチャ上のイディオムか、多くの開発者がすでに直感的に使っている常識的なアプローチかもしれません。
お絵かきアプリの手ぶれ補正という具体例から生まれたパターンですが、データ変換、画像処理、バリデーションなど、他の領域でも応用できるのではないでしょうか。
もし「これは〇〇パターンだよ」「こういう名前で知られているよ」という情報や、ミュータブルなパイプラインの落とし穴などがあれば、コメントでご指摘いただけると幸いです。
Discussion