C
Linux
オブジェクト指向
デザインパターン
Builder

C言語で デザインパターンにトライ! その2. Builder パターン ~面倒な初期化を一手に引き受ける。

はじめに

「C言語でトライ! デザインパターン」
第二弾はBuilderパターンです。C言語はポインターが便利すぎるので、今回は本パターンがC言語でのどんなケースで恩恵を与えられるのか、自分なりに考えた上でライブラリを作ってみました。
拡大解釈が入っていますが、逆に組み込みC開発者的にはイメージしやすいかもしれません。

その1:Flyweight パターン
その3. Observer パターン(出版-購読型モデル)
デザインパターン一覧

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

その2. Builder パターン

クラスでのサンプル:constructorをわかりやすく

wikipediaの説明は以下となります。

オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。

上記を見た時点では別のことを考えていたんですが、サンプルをいくつか見てみた際の印象は、「constructorがごちゃごちゃしてややこしい時は、Directorに一括でやってもらうといいぜ!」というような印象を受けました。
なるほどパラメーターが大量に出てくると、記載が大変だったり、この場合はこのパラメーターは別にいらない!みたいな制御が大変ですよね。
さて、クラスでのサンプルイメージは出来ました。

じゃあC言語でconstructをラップするクラスを用意したいってケース、あるんでしょうか?
…正直あまり思い浮かばないです。

魔法の言葉void *を持ち、ポインタを自由に操れるC言語だと、construct代わりの関数でパラメータが増えるんなら、上手に構造体定義とポインタ制御でやりくりしてる印象です。スキップケースは値が初期値でない or NULLでないなら更新みたいな。
ちなみにC言語開発者のポインタ信仰には別の論理的な理由があります。ポインタって常に4byteなので、沢山の引数の代わりにポインタを使うと関数用に取られる使用メモリ量が変数分⇒4byteに激減するんですよね。構造体を直接渡すと鬼の首を取ったように怒るC開発者がいるのはこの為です。コーディングルールとして定めている現場も多いんじゃないでしょうか?
なので、引数の多いconstructorは当然のように構造体ポインタ化しているケースが多いんじゃないかと思います。

2018/05/01追記
ポインタサイズはすべての環境、OSで共通4バイトではなく、OS, コンパイラ等によって変わるそうです。64bitなら8バイトとか
古いシステムでchar *だけサイズが違うなんてケースもあったんだとか。
大抵ポインタサイズは同一環境内で一定(サイズの保証はない)という認識でよさそうです。

というわけで、constructに限って言うと、既にポインタを使って問題回避しているように思えるC言語。
…変数初期化が複雑にならないからBuilder パターンがいらない?本当にそうでしょうか?

広義の解釈:初期処理をわかりやすく 初期処理の大変な現代システム

私がBuilderパターンを調べてみて、まず真っ先に思い浮かんだことがあります。

例えばとあるシステムでHTTP通信をしたいとします。デバイスは複数

この場合、私は組み込みのミドルウェアより上位の開発がメインなので、OSI参照モデルのネットワーク層~トランスポート層辺りを確立された状態での開発をすることが多いです。

この時、下回りの人的にはドライバをロードしてデータリンク層まで確立するとか、ルーティングを組むみたいな各層の開始トリガーが欲しかったりすることもあるので、ここでこのAPIを呼んでね!みたいなケースになることがままあります。
- あるデバイスではドライバのロードがいるから、デバイス認識の為にloadのAPIから呼んでね!ああ、ethernetはいらないよ。OSがやるからね!
- 接続パラメータはデバイスによって違うよ!変える必要があるなんて当たり前でしょ!
- 全部IPを固定で振りたい?4Gだと基地局からもらうから無理だよ!

ああもうレイヤー順に構築していくだけなのに、微妙に手順が違う。もう!と面倒なことに。

これが逆にシステムに乗せるアプリケーションレイヤーの人目線になると、"どれでもいいからHTTPが使える状態にしろよ!"みたいな感じなんですよね。

要は、こんな感じのシステムにしたいってことか。
- 上位アプリケーション側⇒よくわからんからデバイス準備!ってやればHTTP通信出来るようにしてよ!
- デバイス側⇒ハード都合に合わせて初期処理の有無、初期パラメータは変えてよ!
- ミドルウェア開発者俺⇒何をバカなことを、そんな初期処理を簡単にしつつ違うデバイス(オブジェクト)を生成するデザインなんて、あるわけが!

…あ!
ということで上記イメージでBuilderパターンを適用したライブラリ設計をしました。

ライブラリ

というわけで、上記のイメージでサンプルライブラリ作成。
実際のパラメーターの受け渡しについてはこれだ!ってのが浮かばなかったので、あくまで参考程度としてください。

概要

ちょっと変則的な形です。

  • Builder interfaceの定義はconfファイルによって受け渡す。Builder interfaceの各メソッド定義はここでは固定
  • Builder実装クラスは動的ライブラリ(ここではプラグインと呼びます)。dlopenを利用したのロードで実現
  • Builderの初期化後に使えるinterfaceも提供可能にする。

前述で書いたような複雑な手順はconfファイルに羅列。ライブラリが対応するメソッドを実装クラスから読み込みます。
初期化処理が本ライブラリのconstructor APIでまとめて処理されます。
また、confファイルに書いてあるAPIに関わらず、プラグインは必要な初期化APIだけを実装すれば、本ライブラリが勝手にその処理をスキップします。

動作環境: Ubuntu 18.04 Desktop, Ubuntu 14.04 Desktop, Cent OS5.1 Desktopで動作確認済み

クラス設計

LowerLayerDirectoryのメソッドが本ライブラリのAPIとなります。Builderインターフェイスは自由にconfファイルに記載する形で定義してもらい、プラグイン内で同名APIを定義することで利用可能となります。
さらにプラグインはlower_layer_builder_instance_newという名前のAPIを定義しておくことで、Plugin側で作成したインターフェイスを利用することが可能となります。

builder_lib_real.png

イメージが付きにくいと思うのでサンプルのクラス図を張っておきます。

Builderをconfファイルで表現。Builder実現クラスが.so形式のライブラリになっています。ここではlibusb_device_plugin.so, libosssupport_device_plugin.soの2ライブラリを用意。
また、別途DevicePluginInterfaceを用意。ライブラリはBuilder+α構成。Builder以外にconstruct後の操作インターフェイスとしてDevicePluginInterfaceをプラグイン側で用意しています。

builder_lib.png

いい点

  • 利用者(アプリケーションレイヤー開発者)としては、デバイスの初期化処理を意識しなくてもよい
  • プラグイン開発者は、必要な初期化APIだけ実装すれば勝手に初期化してくれる
  • 初期化はthread実行なので、時間のかかる初期化に利用者がブロックされることはない

使いどころ

  • あるモジュールを導入するための複雑な初期手順のシステムをすっきりさせたい

欠点

  • 使い方がわかりずらいかも
  • initial_parameterを初期化後のinterfaceに渡す仕組みを作っていない(必要?不要?がっつり利用するなら要検討)

詳細

API定義

基本のAPIはこれです。

lower_layer_director.h
#include "lower_layer_builder.h"

//DirectorClassクラスの型名だけ定義
struct director_t;
typedef struct director_t *Director;

//lower_layer_director_newで取得できるDirectorのクラス構成。
typedef struct lower_layer_director_t{
        void * lower_layer_interface;/*操作インターフェイス。定義はプラグイン側と共有する。定義未定なのでvoid *に変更*/
        DirectorClass director;/* directorクラスのポインタ。利用者は直接使わない*/
} lower_layer_director_t, *LowerLayerDirector;

//LowerLayerDirectorの初期処理。builder_lib_nameで指定された.soライブラリをロードし、
//builder_interface_confで定義されたAPIをconstructのAPIとして読み込みます。
LowerLayerDirector lower_layer_director_new(char * builder_lib_name, char * builder_interface_conf);

//constructです。lower_layerの初期処理が完了します。
//結果はinitial_resultに設定したコールバックに通知されます。
void lower_layer_director_construct(LowerLayerDirector director, void * initial_parameter, void (*initial_result)(int result));

//LowerLayerDirectorの解放処理
void lower_layer_director_free(LowerLayerDirector director);

使い方:
1. lower_layer_director_newでプラグインとBuilder interfaceのAPI設定confファイルを指定し、LowerLayerDirectorクラスを生成します。
2. lower_layer_director_constructで初期処理として、設定confファイルに書かれているAPIを上から順に実行します。このとき、Builder interface実装ライブラリに実装されていないものはスキップします。結果はコールバックで通知。Successならlower_layer(サンプルクラス図だとDevicePluginInterface)が使用可能となります。
3. lower_layer_director_freeで使用を終了します。

Builder interfaceのAPI設定confファイルとBuilder interface実装ライブラリの実装クラスについても説明します。

Builder interfaceのAPI設定confファイル

sample.conf
# # はコメント, 後ろの/もコメント
#interfaceとしてプラグインが実装する関数名を順に記載します。
#今は、すべて以下のような定義としています。
# //initial_parameterはpluginによって定義された初期化パラメーターになります。
# int function (void * initial_parameter)
#
# このconfファイルのケースだと、初期化APIとしてllbuilder_initial_name_1, llbuilder_initial_name_2が順に呼ばれます。未実装のAPIはスキップされます。
llbuilder_initial_name_1
llbuilder_initial_name_2

Builder interface実装クラス

lower_layer_builder.h
//初期化APIの戻り値
#define LL_BUILDER_SUCCESS (0)
#define LL_BUILDER_FAILED (-1)

//インターフェイス実装クラスがconfファイルに書かれているもの以外にIntrerfaceを持つなら、new用のAPIを実装します。
//lower_layer_director_newの際にlower_layer_interfaceにそのインスタンスが設定されます。
void *lower_layer_builder_instance_new(void);

//インターフェイス実装クラスの解放処理です。
//lower_layer_director_newの際にlower_layer_interfaceにそのインスタンスが設定されます。
void lower_layer_builder_instance_free(void *interfaceClass);

初期処理後に使えるクラスもプラグインがlower_layer_builder_instance_newで実装することでライブラリ利用者に提供できます。

サンプル

confファイルは以下

device_plugin.conf
llbuilder_load_driver//load driver if need, if OS support this devuce, don't need to implement
llbuilder_initial_device//initialize device, as set IP, ...
#Interface, defined in device_plugin_if.h
  1. llbuilder_load_driver⇒クラスではdeviceを認識する
  2. llbuilder_initial_device⇒deviceにIPを振る(layer2まで) を使ってプラグインを初期化します。

プラグインは2種類。
1. libusb_device_plugin.so⇒USB接続で使用するwifiのデバイス制御ライブラリです。外付けなのでllbuilder_load_driverが必要
2. libossupport_device_plugin.so⇒有線制御ライブラリです。OSがデバイスを認識してくれるので、llbuilder_initial_deviceだけでOK

という風に、必須初期操作が違います。ここは不必要なAPIを実装しなければOKという感じ。

これらで初期化されるパラメーター構造体はサンプル側で用意してあげるイメージなので、利用者はこちらの実体付きでlower_layer_director_constructを叩くと、なんか知らないけどプラグイン側が初期値を詰めてくれるという寸法です。

//有線はIPだけ
typedef struct ossupport_device_ {
        char device_name[64];
        char ipaddress[64];
} ossupport_device_s;
#endif

//wifiはssidとかもある。
typedef struct wifi_device_ {
        char device_name[64];
        char ipaddress[64];
        char ssid[64];
} wifi_device_s;

ただ、このサンプルではlower_layer_director_constructを叩いた時点では、IPが振られる(体)なので、HTTPが使える状態(インターネットに出る)ところまではいきません。
そのため、pluginがインターフェイスを用意してきました。

device_plugin_if.h
struct device_plugin_interface;
typedef struct device_plugin_interface device_plugin_interface_t, *DevicePluginInterface;

struct device_plugin_interface {
        void (*connect)(DevicePluginInterface this);
        void (*disconnect)(DevicePluginInterface this);
};

接続がうまくいった後に、connect, disconnectで外部の通信開始/停止を制御してねという設計。

これにより、利用者的にはとりあえずwifiも有線も初期化しておいて、IF取得
⇒HTTPが使いたくなったらconnectみたいなことが出来ます。

具体的な初期化はわからないけどlower_layer_directorにお任せ~って感じで。

当然これはサンプルなので、プラグインは実際に接続処理をしているわけではなくログ出力だけです。
ただlibossupport_device_plugin.soはOSがいけてないらしく、たまにconnectに失敗しますが。

これらの動作はbuild後に出来るpractice_design_pattern/builder/test以下のtestバイナリで確認できます。
起動オプションなしでwifi, ありで有線のプラグインを使用します。
有線側はllbuilder_load_driverが未実装ですが、動作出来ていることがわかると思います。

コード

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

感想

初期処理をまとめて1つにしてしまうのはよくあるけど、ものによって手番をスキップしたりするという処理が実現できるのは、使いどころがかなりありそうでいいデザインです。
最初見た時はdlopenの使い方紹介にちょうどいいかなくらいに思っていましたが、これはデザインとしてかなり使いやすくていいですね。

API変更履歴

2018/05/03 型定義をvoid *からtypedefで定義した適切な型名へ変更しました。
2018/05/05 DirectorClassを型名だけ宣言する形に変更し、誤って別のポインタを代入できないようにしました。
2018/05/20 APIに対するクラス設計を追加。設計に合わせてlower_layer_director.hのクラス名を修正。コードのURLを変更

参考

オブジェクト指向言語でのBuilderパターンサンプル
https://qiita.com/takutotacos/items/33cfda205ab30a43b0b1
https://qiita.com/disc99/items/840cf9936687f97a482b

・色々動的ライブラリ、dlopenすごい!って書きましたが、差し替えやすい=外部からの潜入口があると言い換えられますので、利用の際は注意が必要です。FTP転送先にプラグインと設定ファイルを置いた日には、もうご自由にハックしてくださいと言っているようなものです。