C
オブジェクト指向
observer
デザインパタン

C言語で デザインパターンにトライ! その3. Observer パターン(出版-購読型モデル) ~好きな時にイベントを発行したいスムーズにこなしたい!

はじめに

「C言語でトライ! デザインパターン」
今回はObserverパターン。一言でいうとイベント登録・発行の為のデザインです。
非同期処理でイベントをポーリングで確認するようなことをしなくていいよう、積極的に使いたいデザインです。

デザインパターン一覧
作成したライブラリパッケージの説明
公開コードはこちら

2018/5/20 API変更履歴を追加しました。API仕様は変わっていませんが説明を追加しています。

その3. Observer パターン(出版-購読型モデル)

wikipediaの説明は以下となります。
https://ja.wikipedia.org/wiki/Observer_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

Observer パターン(オブザーバ・パターン)とは、プログラム内のオブジェクトのイベント( 事象 )を他のオブジェクトへ通知する処理で使われるデザインパターンの一種。
通知するオブジェクト側が、通知されるオブジェクト側に観察(英: observe)される形になる事から、こう呼ばれる。
出版-購読型モデルとも呼ばれる。暗黙的呼び出しの原則と関係が深い。

個人的には出版-購読型(publish-subscribe)でのイメージがわかりやすかったのでこちらで考えます。

  1. とある本の購読者(Subscriber)がいます。
  2. 購読者は、その出版社(Publisher)の出版物を購読(subscribe)登録します。
  3. その後、出版社が本を出版(Publish)するタイミングで、購読者は本が出版されたことを知ることが出来ます。

Subscriberは登録だけしておく。そうすると、Publisherがpublishしたことタイミングで通知を受け取ることが出来る!

イベント型大好き、ポーリング監視が苦手な私にとっては、非同期処理との為に素晴らしい構成です。

ライブラリ

概要

Publisher, Publishそれぞれ向けのライブラリAPIを用意し、利用者としてはイベントを受信する購読者のSubscriberと、イベント発行を行う出版サイドの作成者(writterとしました)がいて、writterがPublishインターフェイスを使ってイベント発行。SubscriberにはSubscriberのインターフェイスによってイベント受信がされます。

クラス設計概要

今回は作成したクラスインスタンスをユーザー側に渡さず、staticなインターフェイスのような形でPublisher, PublishのAPIとして表現します。
実際のインスタンスはライブラリ内で管理されます。ユーザー側が指定したPublisherの数を指定することが出来、0, 1, 2という形でPublisherのIDを指定することでどのPublisherを利用するかを選ぶことが出来ます。
インスタンスを渡してユーザー側のSubscriber, writterが共有する形式だと利用しにくい為、ユーザー側でこのwritter向けは0のように共通のIDを決めて利用することの出来るこのような設計にしました。

Subscribe.png

いい点

・Subscriberはnotifyインターフェイスを通して即座にイベント発行がわかるので、マルチスレッドでの同期処理がスムーズにとれる
・Publish時に登録されているnotifyインターフェイスを順に呼ぶので、イベント発行側に意識させずに割り込み処理を実現できる
・イベント発行元(Publish)とイベント受信先(Subscriber)がはっきり分かれているので、設計しやすい(個人の感想です)

使いどころ
・モジュールが同期の必要なマルチスレッド/プロセスの場合
・イベント発行が頻繁な場合

欠点は思いつきません。使いやすくてSubscribeしすぎてわけがわからない!なんてことないようにしましょうくらいかな。

動作環境: Ubuntu 18.04 Desktopで動作確認済み, 大抵のLinux OSなら動くと思います。

詳細

API定義

ヘッダーを分けようか少し悩みましたが、シンプル構成なので纏めました。

publisher.h
#define PUBLISHER_SUCCESS (0)
#define PUBLISHER_FAILED (-1)

// APIで利用するSubscriberのAccount。定義詳細はライブラリ内実装で行っています。
struct subscriber_account_t;
typedef struct subscriber_account_t subscriber_account_t, *SubscriberAccount;

//publish_contentの数を指定して生成します。publish_contentの識別子はIDで、
//1, 2, ..と連番をつけますので、XXX用のpublish_contentは何番という風に内部で決めてください。
int publisher_new(size_t contents_num);

//作成したpublisherをまとめて削除します。
void publisher_free(void);

//subscribe。
//content_idで使うpublish_contentを指定、publish_typeはイベント種別のor演算としていますので、typeを複数指定できます。
//また、ctxでユーザーデータの指定が出来ます。
SubscriberAccount publisher_subscribe(int content_id, int publish_type, void (*n                                                                                                                                                                             otify)(int publish_type, void * detail, void * ctx), void * ctx );

//1度限定のsubscribe。
//詳細はpublisher_subscribeと一緒
void publisher_subscribe_oneshot(int content_id, int publish_type, void (*notify                                                                                                                                                                             )(int publish_type, void * detail, void * ctx), void * ctx );

//unsubscribe。publisher_subscribeの戻り値を指定して削除します。
void publisher_unsubscribe(int content_id, SubscriberAccount account);

//publishします。content_idでpublish_contentを指定、publish_typeでイベント種別を指定します。
//subscribeされたイベントでpublish_typeが一致しているもののnotifyを実行します。
void publisher_publish(int content_id, int publish_type, void * detail);

使い方:
1. publisher_newでpublisherクラスを生成します。指定した数だけイベント発行元であるpublish_contentが作成されます。
 publish_contentの識別はIDで。1, 2, ..., contents_numと振るので、好きにどれを使うか選択してください。
2. publisher_subscribeでnotify関数(Subscriberインターフェイス)を登録。
3. publisher_publishでイベント発行。typeの一致するpublisher_subscribeで登録されたnotify関数が呼ばれます
4. 最後にpublisher_freeでまとめてリソースを削除

一応publisher_freeした後再度publisher_newしても使えます。

サンプル1

テストコードから抜粋します。使い方のイメージが付くと思います。
まずは初期処理です。

main.c
#define MAX_PUBLISHERTE (3)
#define PULISH_CONTENT_FOR_NORMAL (1)
#define PULISH_CONTENT_FOR_MULTI_TYPE (2)
#define PULISH_CONTENT_FOR_UNSUBSCRIBE (3)
static int testdata_init() {
        if(publisher_new(MAX_PUBLISHERTE) != PUBLISHER_SUCCESS) {
                printf("####1st create failed\n");
                return -1;
        }
...
}

publisher_newでpublisherクラスを生成。
テストでは1.通常, 2.複数のpublish_type登録, 3.+unsubscribeというパターンをとったので、3つのpublish_contentを用意しました。
それぞれで利用するpublish_contentのIDはマクロで固定しちゃいます。

後はnotify関数を定義します。

main.c
typedef struct testdata {
        int notify1_cnt;
        int notify2_cnt;
        int notify3_cnt;
} testdata_t, *TestData;

#define NTYPE(slide) (0x01)<<(slide)

static void test_notify1(int publish_type, void *  detail);
static void test_notify2(int publish_type, void * detail);
static void test_notify3(int publish_type, void * detail);

static void test_notify1(int publish_type, void * detail) {
        printf("%s, %d\n", __FUNCTION__, publish_type);
        TestData testdata = (TestData) detail;
        testdata->notify1_cnt++;
}
static void test_notify2(int publish_type, void * detail) {
        printf("%s, %d\n", __FUNCTION__, publish_type);
        TestData testdata = (TestData) detail;
        testdata->notify2_cnt++;
}

static void test_notify3(int publish_type, void * detail) {
        printf("%s, %d\n", __FUNCTION__, publish_type);
        TestData testdata = (TestData) detail;
        testdata->notify3_cnt++;
}

今回はdetailtestdata_t構造体ポインタを、test_notifyX関数はtestdata->notifyX_cntをカウントアップするようにしました。テストコードでは、publishすると対応するnotifyX_cntが1つ増えるよねという形でテストしています。

実際の利用はこんな感じ。ここではPULISH_CONTENT_FOR_NORMAL (1)のpublish_contentを利用します。

main.c
static int test_normally_subscribe() {
         //Subscribe
        //type=1
        if(publisher_subscribe(PULISH_CONTENT_FOR_NORMAL, NTYPE(1), test_notify1) == NULL) {
                printf("####failed to add test_notify1 subscribe\n");
                return -1;
        }

        //type=2
        if(publisher_subscribe(PULISH_CONTENT_FOR_NORMAL, NTYPE(2), test_notify2) == NULL) {
                printf("####failed to add test_notify2 subscribe\n");
                return -1;
        }

        //type=4
        if(publisher_subscribe(PULISH_CONTENT_FOR_NORMAL, NTYPE(3), test_notify3) == NULL) {
                printf("####failed to add test_notify3 subscribe\n");
                return -1;
        }

        //ここからpublishのテスト
        testdata_t prev_data, current_data;
        memset(&prev_data, 0, sizeof(prev_data));
        memset(&current_data, 0, sizeof(current_data));

        int i=0;
        for(i=0;i<CHECK_TYPE_MAX;i++) {
                publisher_publish(PULISH_CONTENT_FOR_NORMAL, i, &current_data);
                //orをとってpublisher_publishで指定したtypeと一致するかがイベント発行条件なので、typeに0を指定すると全部のイベントが引っかかります。
                if(i==0) {
                        prev_data.notify1_cnt++;
                        prev_data.notify2_cnt++;
                        prev_data.notify3_cnt++;
                //他はSubscribe時のtypeに完全一致した時だけnotifyが呼ばれます。
                } else if(NTYPE(1) == i) {
                        prev_data.notify1_cnt++;
                } else if(NTYPE(2) == i) {
                        prev_data.notify2_cnt++;
                } else if(NTYPE(3) == i) {
                        prev_data.notify3_cnt++;
                }

                //想定通りにnotifyが呼ばれれば、上の条件と同じようにtest_notifyが呼ばれる
                // => publisher_publishで指定したcurrent_dataとprev_dataのデータが一致する。というテストです。
                if(memcmp(&prev_data, &current_data, sizeof(prev_data)) != 0) {
                        printf("%d publish failed\n", i);
                        return -1;
                }
        }
        return 0;
}

ちゃんとpublisher_publish時にtest_notifyXが呼ばれるため、テストコードが途中でエラーになり、return -1されることがないです。
ちなみに上のコメントのように、publisher_publish時に指定するtypeが0だと、利用しているpublish_content内のsubscriber全員のnotifyが呼ばれます。

subscribe時の複数type指定は例えばこんな感じ。

main.c
#define CHECK_TYPE_MAX (0xF)
static int test_multi_type_subscribe() {
        int ntype1=0;
        int i=0;
        for(i=0;i<CHECK_TYPE_MAX;i+=2) {
                ntype1+=NTYPE(i);
        }

        if(publisher_subscribe(PULISH_CONTENT_FOR_MULTI_TYPE, ntype1, test_notify1) == NULL) {
                printf("####failed to add test_notify1 subscribe\n");
                return -1;
        }
        ...
}

このケースでは4ビットの範囲で偶数分ずらしたビットを立ててるので、0, 1, 4, 5のtype指定でpublisher_publishを実行すると、test_notify1が呼ばれます。
また、publish_content指定もPULISH_CONTENT_FOR_MULTI_TYPEに変えているので、test_normally_subscribe時のイベントが呼ばれるなんてこともありません。

サンプル2

せっかくの非同期イベントなので、マルチスレッドのサンプルも書きました。
ざっくり抜粋するので詳細が見たい方はコードを参照してください。

まずメッセージ定義

publish_msg.h
#define PUBLISH_ID (1)
#define PUB_MSG_PUBLISH_NEW_BOOK (0x01<<0)
#define PUB_MSG_PUBLISH_DISCOUNT_BOOK (0x01<<1)
#define PUB_MSG_PUBLISH_STOP_PRODUCTION (0x01<<2)

typedef struct publish_msg_detail {
        char writername[32];
} publish_msg_detail_t;

本に関する通知メッセージを出します。

main側

main.c
int main() {
...
        publisher_new(1);

        printf("Start subscribe!\n");
        subscriber_init();

        while(is_running()) {
                //標準入力により情報を
...
                printf("OK, notify to subscriber!\n");
                publisher_publish(PUBLISH_ID, type, &msg_defail);
        }

        subscriber_exit();
        publisher_free();
}

subscriber側。subscriber_initでsubscribe, スレッド作成します。

subscriber.c
void subscriber_init() {

        //大本のイベントは1つ
        account = publisher_subscribe(PUBLISH_ID, PUB_MSG_PUBLISH_NEW_BOOK|PUB_MSG_PUBLISH_STOP_PRODUCTION|PUB_MSG_PUBLISH_DISCOUNT_BOOK, notification);

        ...
        //ソケットを作ってスレッド間通信可能にします。
        create_socket();

        //スレッド開始
        pthread_create(&tid, NULL, subscriber_main, NULL);
}

notificationではメッセージを送信

subscriber.c
void notification(int publish_type, void * defail) {
        publish_msg_detail_t *msgdetail = (publish_msg_detail_t *) defail;
        //メッセージ作成
        ...
        //送信
        write(WRITE_SOCK, &msg, sizeof(msg));
}

スレッドのmainはよくあるselect⇒readでのメッセージ待ち受けをしてます。

subscriber.c
static void * subscriber_main(void *arg) {

        subscriber_msg_t msg;

...
        //ループを抜けられるようis_runningを作成
        while(is_running()) {
...
                memset(&msg, 0, sizeof(msg));
                int ret = read(READ_SOCK, &msg, sizeof(msg));
                if(ret < 0) {
                        SUBSCRIBER_LOG("failed to select, %s\n", strerror(errno));
                }
                subscriber_msg_action(&msg);
        }

        close(READ_SOCK);
        pthread_exit(NULL);
}

こんな風に登録イベントでは別スレッドにメッセージ送信⇒メッセージ待ちしていたスレッドで即座に動作
という使い方が一番安定するんじゃないかなと思います。
IDやpublish_typeによってnotification関数を変え、メッセージ送信先を増やしてあげても面白そうです。

コード

以下に置いてあります。
https://github.com/developer-kikikaikai/design_pattern_for_c/tree/master/publisher

感想

イベントタイミングでの通知系の同期の取り方は好きなので、個人的にはすぐにでも利用したいものになったと思います。
一番のメリットはマルチスレッドかなと思ったのでサンプルも作って自分が使いたい時に流用しやすくしましたが、その時そのタイミングで即座に行いたい処理を登録ってのも考えられそう。
利用頻度が高そうです。

API変更履歴

2018/05/20 APIに対するクラス設計解説を更新。コードのURLを変更
2018/07/21 ユーザーデータが指定可能なように修正。APIにoneshotを追加