はじめに
「C言語でトライ! デザインパターン」
今回はChain of Responsibilityパターンです。長いのでここでは基本CoRパターン、CoR等と呼ぶことにします。今回はどんなものかの説明にいくつか例を用いた上で、ライブラリの説明をします。例だしが多くなったので、先に所感だけまとめておきます。
・大きな特徴は順番が保証されること、自身が処理しきれたらそこで終わりに出来るが挙げられるのかなと感じました。これは長所にも短所にもなる点で、例えば順番が保証⇒
- いい点:処理が入れ替わらないので想定外の割り込みが発生しない
- 悪い点: 並行処理できないので処理が多いほど遅くなる
と、使い方次第で印象が大分変わります。利用しようと思えば出来るケースは結構ありそうなので、きちんとメリットのあるタイミングでの利用を意識したいです。
デザインパターン一覧
作成したライブラリパッケージの説明
公開コードはこちら
2018/5/20 API変更履歴を追加しました。API仕様は変わっていませんが説明を追加しています。
その5. Chain of Responsibility パターン
wikipegiaより抜粋。
Chain-of-responsibility パターン, CoR パターンは、オブジェクト指向設計におけるデザインパターンの一つであり、一つの コマンドオブジェクトと一連の 処理オブジェクトから構成される。各処理オブジェクトは、処理できるコマンドオブジェクトの種類と、自身が処理できないコマンドオブジェクトをチェーン内の次の処理オブジェクトに渡す方法を記述する情報を保持する。また、新たな処理オブジェクトをチェーンの最後に追加する機構を備える。
MVP(Model-View-Presenter)、表示を司るView, Viewを管理するPresenter, 画面表示の元ネタ、操作となる処理を行うModelという3つの構成からなる、UI向けのデザインの1つです。
後でMVPの話はするとして、このViewとPresenterの関係もMediatorパターンと似たような考え方をしていると思いました。
ちょっと広義な解釈ですが、イメージとしてはこうかなと。
- Interfaceクラス(複数も可) の実装クラスリストを持ったクラスがあります。ここではrootクラスとでも呼びましょうか。使えるメソッドはInterfaceクラスと同じ。
- ユーザーがあるメソッドをコールすると、rootクラスは先頭のInterface実装クラスの同メソッドを呼びます。
- Interface実装クラスの処理後、実装クラスもしくはrootクラスは、次のInterface実装クラスを呼ぶかどうかを判断します。自分の処理で役割を終えているならここで終了。そうでないなら次のクラスの同メソッドを呼びます。
- 3を終端か転送終了条件を満たすまで繰り返します。
用途が幅広そうなデザイン。いくつか一般のツールで利用されているケースが浮かんだので、例に出して説明します。デザインパターンについて詳しい方から見て違和感のある解釈をしていたら申し訳ありません。
一般利用ケース1: HTTPサーバー OSS lighttpd
のプラグイン
HTTPサーバーOSSにlighttpdというものがあります。このlighttpd, 共有ライブラリをプラグインとして追加することが出来るのですが、このプラグインの利用方法がCoRパターンになっています。
まずlighttpdがプラグインを認識する仕組みについて簡単に。
lighttpdはplugin.hというヘッダーにinterfaceクラスのような関数ポインタ―群を用意しています。
typedef struct {
size_t version;
buffer *name; /* name of the plugin */
void *(* init) ();
...
handler_t (* handle_uri_clean) (server *srv, connection *con, void *p_d); /* after uri is set */
...
} plugin;
plugin実装側は、ライブラリ名_plugin_init
(mod_auto.so
ならmod_auth_plugin_init
)を定義し、その中でplugin
構造体の関数ポインタに関数を設定します。するとlighttpd側が設定ファイルからライブラリを探し出し、dlopenを利用してプラグインの関数を認識します。
その後、必要なタイミングで設定したプラグインの関数を利用するのですが、この利用方法がCoRパターンしてます。
ルールは単純に言うと、プラグインの関数がHANDLER_GO_ONを返したら次のプラグインへ、HANDLER_FINISHEDを返したらその場でHTTP responseを返す。
厳密にはもっと色々な使い方がありますが、簡単には上記のような形です。
例えばmod_auth
, mod_cgi
, mod_fastcgi
の順に設定ファイルに記載されていたとしましょう。
順に認証、cgiコマンド利用、fastcgiサービス利用の為の標準プラグインです。
この状態で例えば、
-
アクセス権限のないユーザーがアクセスしてきた場合:
-
最初のmod_authの関数が呼ばれ、認証チェック
->認証エラーなので HANDLER_FINISHEDを返し、この時点で401で応答。
-
最初のmod_authの関数が呼ばれ、認証チェック
-
アクセス権限のあるユーザーがアクセスしてきた場合:
- 最初のmod_authの関数が呼ばれ、認証チェック
->**認証OKなのでHANDLER_GO_ONを返す
-> mod_cgiが実行される。 - mod_cgiはurlから自分あてのrequestか判定し、自分あてなら処理してHANDLER_FINISHED。そうでないならHANDLER_GO_ONを返してmod_fastcgiへ
- 最初のmod_authの関数が呼ばれ、認証チェック
というように、プラグインがHTTP requestの処理が出来るよう、情報の伝達を行います。自作のプラグイン追加用途もありますが、
認証やHTTPSの復号といった、アプリケーションが意識したくない処理は先にプラグイン登録して処理してしまえるというメリットもあります。
一般利用ケース例2: Androidのタッチイベント
パターンの話を見て最初に思いついたのがこれでした。
スマホのアプリをイメージしていただきたいです(私はAndroidユーザーなのでAndroidのイメージ。iPhoneの操作と違ったらすいません)。
例えばHome画面でアプリアイコンを触るとアプリ起動、長押しするとそのアイコンが動かせるようになると思います。
このように、同じ場所に触れた時に、そのアイコンが動作するのか、それとももっと下に位置しているホーム画面が反応するのか、操作によって動作するものが変わります。
この仕組みを表現しているonTouchEvent
という仕組みが、CoRパターンの思想で作られているとに当てはまるのではないかと思っています。
実際にAndroidのタッチイベントを説明されている方のサイトの説明、図をお借りします。
白い大枠や赤、緑の枠(Layout
)、Button
(View
)が重なっている場合を考えます。
Androidでは奥が親、手前が子の関係です。微妙に重なってないものは親子関係にはないので、リストはこのような状態。
FrameLayout1(白)
|-- FrameLayout2(赤)
`-- FrameLayout3(緑)
`- Button
このLayout
, View
ですが、ともに同じインターフェイスクラスを継承しており、以下のメソッドを持っています。
-
dispatchTouchEvent
- イベントを子に伝送する。子がいない場合はonTouchEvent
を実行する。 -
onTouchEvent
- イベントの処理を実装する。この戻り値でタッチイベントが終了 or 次(onDispatchTouchEvent
呼び元)のonTouchEvent
が実行される。
今回の例の場合、こんな順番でそれぞれのメソッドが呼び出されます。同階層で重なっている赤、緑は上の緑が優先です。
改めてシーケンスで表すとこんな感じ。
FrameLayout1(白)
.dispatchTouchEvent()
|
|----------> FrameLayout3(緑)
| .dispatchTouchEvent()
| |
| |----------> Button
| | .dispatchTouchEvent()
| | |
| | |--> Button
| | | .onTouchEvent()
| | | |
| | |<-------/
| | 終了判定!!
| | |
| |<-------------/
| |
| |--> FrameLayout3(緑)
| | .onTouchEvent()
| | |
| |<-------/
| 終了判定!!
| |
|<--------------/
|
|----------> FrameLayout2(赤)
| .dispatchTouchEvent()
| |
| |--> FrameLayout2(赤)
| | .onTouchEvent()
| | |
| |<-------/
| 終了判定!!
| |
|<--------------/
|
|--> FrameLayout1
| .onTouchEvent()
| |
|<-------/
|
<-------/
-
FrameLayout1(白)
->FrameLayout3(緑)
->Button
->の順でonDispatchTouchEvent
が実行。CoRパターンですが、子がいるとすぐ子のonDispatchTouchEvent
に渡してしまいます。 - 子がいなくなったらここで
onTouchEvent
を実行。 -
onTouchEvent
は処理実行後、戻り値でタッチイベントを終了するか、ひとつ前に戻る(onDispatchTouchEvenからonTouchEvent)かが決まります。
一気に末端まで移動してしまうので不思議な感じですが、onDispatchTouchEvent
という操作を受け渡し、onTouchEvent
実行結果によってという形で終了を判定するというCoRパターンなのかなと思います。
一般利用ケース3?: Jenkins
「自身が処理できない場合に次に渡す」と逆の発想ですが、"自動化"というキーワードで必ず出てくるjenkinsも仕組みとしては似ているのかなと思いました。
自分で定義したスクリプトであるジョブを順々に実行し、エラーがあった場合に処理を止めてメールを出すといったことが出来るため、ビルド後のテスト処理を行っているプロジェクトも多いのではないでしょうか。
これも見方によっては「エラーの発見したら処理を止める」という責務を持ったジョブたちのchain構造ととれば、CoRと思えないでしょうか。
- 「ジョブを実行する」という操作を順次実行するよう設定を行う
- エラーを発見したジョブは責務を遂行。エラー情報を展開し、テストを中断する。(場合によっては継続)
- ジョブの追加があるなら次の実行ジョブとして追加が可能
デザインパターンとしては少しずれているのかもしれませんが、こういったテストや自動化といった観点での使い方も面白いとは思います。
ライブラリ
ここからはライブラリ紹介。単純なのでさっくり行きます。
概要
今回は名前をキーにして関数を登録。call時はその関数を順々に呼び、戻り値で途中終了するかを判定する形をとりました。
クラス設計概要
クラス設計はこのような形。Interfaceクラスで表現している箇所の実現方法は正直唯のAPIです。ChainOfResponsibilityのインスタンスもライブラリ内で保持。
APIユーザー側のChainOfResponsibilityInterfaceに指定するnameをキーに、同じChainOfResponsibilityインスタンスを共有する形にしています。
そのため、内部でflyweightパターンで作成したライブラリを利用しています。
クラス図での表現はちょっと仰々しいかもしれませんが、実際のAPIとしては割とシンプルだと思います。
いい点
- 追加の仕方を単純にしてあるので、後からの処理追加が楽。
- 入力パラメーターがポインターなので、データを構築していくようなシステムにも利用可能
- Chainなので、処理が必ず追加した順番に実行される(シーケンスが保証される)
使いどころ
- 発生イベントに対する動作対象となるものが多い場合。まとめてグルーピングした相手にイベントを発行できます。
- 夜間での自動テストのような、延々単純作業を繰り返し、問題が発生したら止める。といったシステムにも相性がいいです。
欠点
- 処理順は決まっており、途中で処理終了となる可能性があるので、自分が登録した関数までイベントが回ってこない可能性がある。
- イベントの用途も対象のクラス・関数も決まっている場合は別の方法が適切だと思います。
- 順々に処理するため、速度や負荷分散を求める場合にはあまり向かないです。
詳細
###API定義
//登録関数のreturn値定義。CoR_GONEXTを返す関数にすると、次に登録された関数に処理が移ります。
typedef enum {
CoR_GONEXT,/*!< go to next */
CoR_RETURN,/*!< exit to call chain_api*/
} cor_result_e;
struct chain_element_part;
typedef struct chain_element_part * ChainElementPart;
//実際の関数定義。入力はvoid *にしてあるので用途に応じて適切に設定ください。
//ユーザーデータをctxで指定可能です。
typedef cor_result_e (*chain_func)(void *arg, void *ctx);
//スレッドセーフにしたい場合はこちらをコールしてください。
void cor_set_threadsafe(int is_threadsafe);
//関数をchainに追加します。IDをキーに同じchainとみなします。
//応答はピンポイントで関数を削除したい場合に使います。
ChainElementPart cor_add_function(const int id, chain_func func, void *ctx);
//登録された関数たちを順々に実行します。登録関数がCoR_RETURNを返すか、リストの最後まで行ったら終了です。
void cor_call(const char *name, void *arg);
//登録された関数を削除します。同名関数は全削除です。
void cor_remove_function(const int id, chain_func func);
//ピンポイント削除用です。cor_add_functionの応答を指定します。
void cor_remove_chain_element_part(const int id, ChainElementPart element);
//最後にchainを丸ごと全部削除します。
void cor_clear(void);
使い方:
-
cor_add_function
で関数を追加します。関数定義はchain_func
に従います。 -
cor_call
で登録されている関数を頭から順に実行します。このとき登録関数がCoR_RETURNを返すとそこで処理が終了します。 - 使わなくなった関数が出てきたら
cor_remove_function
で削除可能です。 - 最後は
cor_clear
で全登録関数のリソースを解放します。
同じChainかどうかは引数のnameをキーにする形式。
内部的にはFlyweightパターンを利用しているため、nameが同じものに対しては同じ関数リストを使用する形になっています。
(そのためcor_clearでまとめて削除する形にしています。)
コード
以下に置いてあります。
https://github.com/developer-kikikaikai/design_pattern_for_c/tree/master/chain_of_responsibility
サンプルは色々なケースを紹介したので省略します。
履歴
2018/07/21 API仕様の微修正 ユーザーデータ追加とピンポイント削除の追加
##参考
Android のタッチイベントを理解する(その1)
http://blog.lciel.jp/blog/2013/12/03/android-touch-event/