LoginSignup
2
1

More than 5 years have passed since last update.

C言語で デザインパターンにトライ! その1. Flyweight パターン ~リソースを意識せず流用する。

Last updated at Posted at 2018-04-24

はじめに

せっかくgithubにアカウントを作ったので、
どうせならいつでもどこでも流用できる自作コードを自分の知識を枯れるまで作ってやろう!

で、どうせなら最近気になっているデザインパターンを参考にしてみよう!ということで、
「C言語でトライ! デザインパターン」
というテーマで、デザインパターンを参考にしてライブラリ設計、作成にトライしていこうかなと。

Cで作ろうと思ったのは、慣れてるのもあるのですが、「デザインパターンを実現しました、やったぜ!」
じゃなくて、「このライブラリ(デザイン)って、こんなところがいいよね!」というところを考えたいからです。
デザインに対して「これはいいね」「こういう点は危ないかも」みたいなことが見えてくるかなと。

大分脳内変換された設計になってるかもしれませんが、ご了承ください。

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

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

その1. Flyweight パターン

デザインパターンの概要をパラパラ眺めていて、一番馴染みのあったのがFlyweightパターン。

Flyweight パターン(フライウェイト・パターン)とは、GoFによって定義されたデザインパターンの1つである。 等価なインスタンスを別々の箇所で使用する際に、一つのインスタンスを再利用することによってプログラムを省リソース化することを目的とする。

インスタンスをプールする管理者がいて、そこにget requestをすればリソースが返ってくる。
newしたかどうかは利用者は気にしないって感じ。
rubyやjavaで実装する時に、毎回newするのうざいなと思ってよく使っています。

当然共通のリソースをみんなで扱うので、ガツガツ変更するようなものは不適切。グローバルのデータですしね。

rubyやjavaで使うケースは、getしたインスタンスのメソッドを即実行みたいなケースが多いけど、
Cだとクラスのようにインスタンスにメソッドがないので、旨味が薄くなりそうな気も。
ちゃんとクラス設計すればC言語でも有用なものになりそうです。

クラスの等価インスタンスリソースを再利用

が特徴のライブラリ設計にしたいです。

ライブラリ

@tenmyo さんからのコメントを元に、ちゃんとconstructorのパラメーターによるリソース流用の出来るライブラリにします。
(@tenmyo さん、丁寧な説明ありがとうございました。)

概要

Flyweightパターンに倣い、getメソッドの引数と等価なリソースを確保・流用するライブラリとなります。
利用者はクラス情報と等価判定の==関数を登録することで、後は引数付きのgetメソッドを使うだけで等価なリソースが取得できます。

また、リソース管理はライブラリ内で行っているため、メモリリークを気にせずデータアクセスが可能です。

説明が長くなるので先にポイントだけ

いい点
- 利用者は必要な時にgetするだけで、ライブラリが等価なリソースがあるか判定してリソース確保/流用をしてくれる。
- 等価かどうかの判定条件は利用者が自由に定義できる。
- 生成されるインスタンスはconstructor/setter/destoructorの設定が出来る。
- メモリの解放を気にしなくてよい。(まとめて解放可能)

使いどころ
- みんなで共有して使いたいimmutableオブジェクトを持ちたい!

欠点
- getしてくるリソースはvoid *なのでキャストが面倒。

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

詳細

クラス設計

クラス設計。API利用ユーザーは利用するクラス用のFlyweightFactoryを生成。その際にFlyweightMethodsインターフェイスを実装してもらいます。
このequall_operandが一致するインスタンスを内部で流用します。
FlyweightFactory生成後はFlyweightインターフェイスのメソッドを介して取得、設定します。
flyweight_jp.png

クラス図としてはUserDefineFlyweightMethodsと表現させてもらいましたが、実際にユーザー側がAPI利用の際に用意するのは以下。これをnew時に指定します。

  • インスタンスを利用/流用したいクラスとして構造体を定義
  • FlyweightMethodsインターフェイスとして関数を定義

API定義

flyweight.h
struct flyweight_methods_t {
        void (*constructor)(void *this, size_t size, void *input_parameter);/*! 引数付きコンストラクタの関数ポインタ */
        int (*equall_operand)(void *this, size_t size, void *input_parameter); /*! 等価条件を判定する関数のポインタ */
        int (*setter)(void *this, size_t size, void *input_parameter); /*! setterの関数ポインタ */
        void (*destructor)(void *this); /*! デストラクタのsetterの関数ポインタ */
};
typedef struct flyweight_methods_t flyweight_methods_t, * FlyweightMethodsIF;

//FlyweightFactoryの生成関数。ここでユーザーが定義した生成クラスのサイズとFlyweightMethodsインターフェイスを登録する。
//例えば同じ構造体だけど等価条件を変えたい場合には、equall_operandに別の関数を設定し、再度この関数を呼ぶ。
//returnでクラスを表すFlyweightFactoryインスタンスとしてポインタが返る。
FlyweightFactory flyweight_factory_new(size_t class_size, int is_threadsafe, FlyweightMethodsIF methods);

//リソース取得関数。flyweight_factory_newで生成したFlyweightFactoryインスタンスと、constructorの引数 constructor_parameterを設定する。
//このconstructor_parameterとクラスのインスタンスをクラス定義時のequall_operandで判定。同じものは使いまわす。
void * flyweight_get(FlyweightFactory this, void * constructor_parameter);

//setter。あってもconstructorのパラメーター以外も変更できるように
int flyweight_set(FlyweightFactory this, void * constructor_parameter, void * data, int (*setter)(void *this, size_t size, void *input_parameter));

//FlyweightFactoryの解放。flyweight_getで確保したクラスインスタンスのリソースもまとめて解放する。
void flyweight_factory_free(FlyweightFactory this);
#endif/*FLYWEIGHT_*/

使い方
1. flyweight_factory_newで、Flyweight経由で生成したいクラスを定義する。(equall_operandを等価判定に利用。ちなみに1を返す関数を登録するとシングルトンになります。) (リンクはwikipedia)
2. flyweight_getで登録したクラスのリソースを取得。引数とインスタンスをequall_operandを利用して比較し、同じだったら同じインスタンスを返す。なければ新規作成
3. flyweight_factory_freeでまとめて解放

ポイントはflyweight_factory_new時のequall_operand。
このequall_operandを利用して、”何が同じリソースか?”をflyweight_getの引数から判定します。

コード

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

サンプル

コードのflyweight/testを改変して説明します。
例えば、

//登録クラス
struct testclass1 {
        int id;
        char *name;
//その他パラメーター
};

//コンストラクタパラメータ
struct testclass1_input {
        int id;
        char *name;
};

//Operand, IDが同じならOK
static int equall_operand_onlyid(void *this, size_t size, void *input_parameter) {
        struct testclass1 * class_instance = (struct testclass1 *)this;
        struct testclass1_input *input = (struct testclass1_input*)input_parameter;
        return (class_instance->id == input->id);
}

static flyweight_methods_t onlyid_method={
        .constructor=constructor_member,
        .equall_operand=equall_operand_onlyid,
        .setter=set_for_operand_onlyid,
        .destructor=destructor_member,
};

//Operand, ID, name両方同じじゃないとダメ
static int equall_operand_member(void *this, size_t size, void *input_parameter) {
        struct testclass1 * class_instance = (struct testclass1 *)this;
        struct testclass1_input *input = (struct testclass1_input*)input_parameter;
        return (class_instance->id == input->id) && (strcmp(class_instance->name, input->name)==0);
}

static flyweight_methods_t member_method={
        .constructor=constructor_member,
        .equall_operand=equall_operand_member,
        .setter=NULL,
        .destructor=destructor_member,
};

...

        FlyweightFactory handle = flyweight_factory_new(sizeof(struct testclass1), is_threadsafe, &member_method);
        FlyweightFactory handle2 = flyweight_factory_new(sizeof(struct testclass1), is_threadsafe, &onlyid_method);

と、testclass1 構造体に対して、2種類のクラス登録を行います。

handleid, nameが同じインスタンスは同じもの
handle2idが同じインスタンスは同じもの

そうすると、

int test_methodsclass(int is_threadsafe) {
        int testcnt=0;
        struct testclass1_input setting[TESTDATA_LEN] = {
                {1,"ootani"},
                {1,"sakai"},
                {1,"ootani"},
                {0,"ootani"},
        };

        void * instance1, *instance2, *instance3, *instance4;
        instance1 = flyweight_get(handle, &setting[0]);//{1,"ootani"},
        instance2 = flyweight_get(handle, &setting[1]);//{1,"sakai"},
        instance3 = flyweight_get(handle, &setting[1]);//{1,"ootani"},
        instance4 = flyweight_get(handle, &setting[1]);//{0,"ootani"},
        //この場合は、id, 名前がともに違うinstance1, 2, 4が違うインスタンス、1と3が同じインスタンスとなる。

        instance1 = flyweight_get(handle2, &setting[0]);//{1,"ootani"},
        instance2 = flyweight_get(handle2, &setting[1]);//{1,"sakai"},
        instance3 = flyweight_get(handle2, &setting[1]);//{1,"ootani"},
        instance4 = flyweight_get(handle2, &setting[1]);//{0,"ootani"},
        //こちらはidが同じであればいいので、instance1, 2, 3が同じインスタンス、4だけ違うインスタンスとなる。

と、同じコンストラクタパラメーターでも、class定義の仕方で流用するリソースが変わります。

前回作ったライブラリは「適当にリソース確保するから、後は使う側頑張れ!」って感じだったのに対し、
こちらはちゃんと同じものに対してgetすれば、ライブラリがリソースを使いまわせるようになりました。
これなら実用性あるな!

感想

コメントをいただき、自分でも有効な使い道がありそうなライブラリになりました。
C言語でのインターフェイス実現も出来るので、登録するクラスにはコンストラクタでメソッド設定をしてあげれば、色々なケースで利用できそうです!

API変更履歴

2018/05/03 APIのハンドルをClassHandleに定義変更
2018/05/05 ClassHandleの型を明記。flyweight_define_classが消えていたので修正
2018/05/20 APIに対するクラス設計を追加。設計に合わせてAPI名を修正。コードのURLを変更


....

ここからは初版ライブラリに対する振り返りです。
最初に作ったプロトタイプはこんな感じでした。

以前のAPI定義

flyweight.h
struct flyweight_init_s {
        void (*constructor)(void *src); /*! コンストラクタの関数ポインタ */
        int (*setter)(void *src, size_t srcsize, void *dist); /*! setterの関数ポインタ */
        void (*destructor)(void *src); /*! デストラクタのsetterの関数ポインタ */
};
//クラス登録関数。
int flyweight_register_class(size_t class_size, int is_threadsafe, struct flyweight_init_s *methods);

//クラス登録解除関数。登録時のidを指定
void flyweight_unregister_class(int id);

//リソース取得関数。登録時のidを指定
void * flyweight_get(int id);

//リソース設定関数。登録時のidを指定, dataは登録時の構造体でなくてもOK, setter関数を新たに指定可能 
int flyweight_set(int id, void * data, int (*setter)(void *src, size_t srcsize, void *set_data));

//リソースをまとめて全開放
void flyweight_exit(void);

//どうしてもflyweight_get中にflyweight_setされたくない場合に使う。
void flyweight_lock(int id);
void flyweight_unlock(int id);

リソースと対応するIDが返ってくる仕組みで、IDをみんなで共有しておけばそのリソースを使いまわせるという仕組みでした。

どこが今一つだったのか?

結局アドレスがIDに変わっただけで、ライブラリが勝手に決めたIDを
利用者が管理、共有しないとリソースを使いまわせない。

アドレスをIDに変えたので、ちょっとは実体をラップするというイメージがしやすいかもしれないけど、
それも使い手の設計次第。Flyweightをっていう前に、利用者に委ねるものが多すぎるなって改めて思いました。

自分で使うかと聞かれるとどうかな。。。って書いたのもこの辺かなと思った。
多機能を提供してないのに、自由に使っていいですよ!と言われましても。。。

とは言っても副作用的にいい面もあったかなと。
malloc/freeを隠ぺいした上でサンプルコードを書いたおかげで、C言語のカプセル化の例が少しイメージしやすかったかなと。

以前のコード

以下にとってあります。
https://github.com/developer-kikikaikai/practice_design_pattern/tree/try_flyweight_1st/flyweight

前回のライブラリサンプルで書いていたC言語でのカプセル化も残しておきます。

参考: C言語でのカプセル化サンプル

まずは名前と年齢を持つhuman_classの登録をします。

human_class.c
struct human_class {
        //private
        char name[NAME_MAX];
        unsigned int age;
};

//init human, return human class id
int human_new(char *name, unsigned int age) {
        struct human_class human;
        //struct human_class を登録。
        int id = flyweight_register_class(sizeof(struct human_class), 0, NULL);

        sprintf(human.name, "%s", name);
        human.age = age;
        printf("  [human new human: %s,%d\n", human.name, human.age);

        //name, ageを設定。
        //flyweight_setはデフォルトだと指定サイズ分memcpyします。
        flyweight_set(id, &human, NULL);
        return id;
}

flyweight_register_classでサイズを指定して登録。
で、flyweight_setで中身をセット。setterを何も指定していないとmemcpyします。

以降このhuman_classのリソースはflyweight_register_classの戻り値で管理。

ここではhuman_newはhumanクラスのnew APIのような役割になっていますね。

確保したリソースの取得はflyweight_getです。

human_class.c
//get human name
unsigned int human_get_age(int id) {
        struct human_class * human = (struct human_class *)flyweight_get(id);
        return human->age;
}

char * human_get_name(int id) {
        struct human_class * human = (struct human_class *)flyweight_get(id);
        return human->name;
}

ここではhuman_get_name, human_get_ageというhumanクラスのPublic APIのようなAPI内部で、
human_classの共有リソースを取得しメンバーを渡しています。

構造体がカプセル化されていますね。

次にhuman_classを保持しているcity_classの例
humanのリソース(ID)をリストで保持しています。

city_class.c
struct city_class {
        char name[NAME_MAX];
        int resident_max;//max size
        int resident_num;//num
        int *residents;//residents, human_classのIDリスト
};

//setterでresident_maxを指定した際にcalloc
static int city_setter_cityclass(void *src, size_t srcsize, void *dist) {
        struct city_class * city = (struct city_class *)src;
        memcpy(city, dist, srcsize);
        city->residents = calloc(city->resident_max, sizeof(int));
        return 0;
}

//flyweight_unregister_class or flyweight_exit時に呼ばれる。destructorの定義も出来ます。
static void city_destructor(void *src) {
        struct city_class * city = (struct city_class *)src;
        free(city->residents);
}

//cityクラスのnew
int city_new(char *name, int resident_max) {
        //setter,  destructorにcity_setter_cityclass, city_destructorを設定
        struct flyweight_init_s methods={
                NULL,//no constructor
                city_setter_cityclass,
                city_destructor
        };

        //city_classと、setter, destructorを登録。
        int id = flyweight_register_class(sizeof(struct city_class), 0, &methods);

        struct city_class city;
        memset(&city, 0, sizeof(city));
        snprintf(city.name, sizeof(city.name), "%s", name);
        printf("[city] new city: %s\n", city.name);
        city.resident_max = resident_max;
        //set class
        //cityクラスの設定更新。city_setter_cityclassが呼ばれます。
        flyweight_set(id, &city, NULL);

        return id;
}

こんな感じでsetter, decstoructorの登録も出来ます。

human_classはこんな感じで利用しています。

city_class.c
struct city_resident {
        char name[NAME_MAX];
        unsigned int age;
};

//humanを追加する際のsetter, flyweight_set時の第二引数がdistに入る
static int city_setter_add_resident(void *src, size_t srcsize, void *dist) {
        struct city_class * city = (struct city_class *)src;
        struct city_resident * resident = (struct city_resident *)dist;
        //humanをリストに追加
        city->residents[city->resident_num++] = human_new(resident->name, resident->age);
        return 0;
}

//humanの追加 API
void city_accept_new_resident(int city, char *name, unsigned int age) {
        struct city_resident resident;
        char *cityname = city_getter_name(city);
        printf("[city] come %s in city: %s\n", name, cityname);
        snprintf(resident.name, sizeof(resident.name), "%s", name);
        resident.age = age;
        //setter指定
        flyweight_set(city, &resident, city_setter_add_resident);
}

こんな感じに、human_classの構造を知らなくてもhuman_newで登録が出来、そのあとhuman_get_name, human_get_age等で情報が取得出来ます。

2
1
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1