はじめに
「C言語でトライ! デザインパターン」。今回はBridgeパターン。
こちらはいきなり適用を意識するのは難しく、拡張する方向性の見えている来た新規開発 or 実際に既存機能の拡張を行う際に意識すると効果のある考え方なのかなという気がしました。
デザインパターン一覧
作成したライブラリパッケージの説明
公開ライブラリコードはこちら
ライブラリにしていないコードはこちら
Bridge パターン
いつものようにWikipediaより抜粋。
Bridge パターン(ブリッジ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 「橋渡し」のクラスを用意することによって、クラスを複数の方向に拡張させることを目的とする。
正直これを見た時点ではBridgeパターンを「拡張の為にちょっと手間を加えるだけなのね」と思っていました。
でも、真面目に向き合うとこのパターンの適応対象があるある過ぎて、常に「新規機能」とか「拡張」⇒ブリッジいる?と意識する必要があるようなデザインな気がします。
適用ケースは「新規機能」・「機能拡張」の場に立った際、この一言で疑念を感じた場合。
- その機能、そのまま継承させていいの?
サンプルコード
TECHSCORE(テックスコア)さんのソートクラスを例にとり、実際にコードを組んでみました。
拡張前のベースとなるソートクラスを実装する
以下イメージでintのリストをソートするクラスを作ります。コードはこちら。
ヘッダー定義はこんな感じ。BaseSorter_tを使ってintのバブルソートとクイックソートを実現します。
struct BaseSorter_t;
typedef struct BaseSorter_t *BaseSorter;
struct BaseSorter_t {
/*ソートメソッド*/
void (*sort)(BaseSorter this);
/*表示メソッド*/
void (*show)(BaseSorter this);
};
/*継承用定義*/
#define BASE_SORTER \
void (*sort)(BaseSorter this);\
void (*show)(BaseSorter this);
/*バブルソート実行クラス*/
BaseSorter bubble_sorter_new(int *num_list, int num);
void bubble_sorter_free(BaseSorter this);
/*クイックソート実行クラス*/
BaseSorter quick_sorter_new(int *num_list, int num);
void quick_sorter_free(BaseSorter this);
実処理を抜粋。メンバーは隠ぺいしてこのファイル内で閉じるようにしていますが、内部処理は全部intに依存しています。
拡張の方向性によっては新しいクラスを作る際にそのメンバー向けのソート処理を実装しなきゃいけなくなる可能性もあるかも。
/*ソート対象のint*/
typedef struct SorterImpl_t {
BASE_SORTER;
int *num_list;
int num;
const char * name;
} *SorterImpl;
static void swap(int *num_list, int place) {
int stash_val = num_list[place];
printf("\tswap %d and %d at %d\n", num_list[place], num_list[place + 1], place);
num_list[place] = num_list[place + 1];
num_list[place + 1] = stash_val;
}
static void bubble_sort(BaseSorter this) {
SorterImpl instance = (SorterImpl)this;
int i, j;
for(i=0; i<instance->num-1; i++) {
for(j=1; j < instance->num - i; j++) {
/*intのソート*/
if( instance->num_list[j] < instance->num_list[j -1] ) {
swap(instance->num_list, j-1);
}
}
}
}
static void quicksort(int *a, int left, int right) {
/*wikipediaコードの転記なので省略*/
}
static void quick_sort_base(BaseSorter this) {
SorterImpl instance = (SorterImpl)this;
quicksort(instance->num_list, 0, instance->num - 1);
}
/*略*/
BaseSorter bubble_sorter_new(int *num_list, int num) {
SorterImpl instance = malloc(sizeof(*instance) + sizeof(int)*num);
if(!instance) return NULL;
instance->num_list = (int *)(instance + 1);
instance->sort = bubble_sort;
/*…略*/
return (BaseSorter)instance;
}
void bubble_sorter_free(BaseSorter this) {
free(this);
}
BaseSorter quick_sorter_new(int *num_list, int num) {
SorterImpl instance = malloc(sizeof(*instance) + sizeof(int)*num);
/*…略*/
instance->sort = quick_sort_base;
/*…略*/
return (BaseSorter)instance;
}
void quick_sorter_free(BaseSorter this) {
free(this);
}
main処理はこんな感じ。main側との依存関係はあまりないので、外部に提供しているメソッドの構成は問題なさそう。
int main() {
int num_list[]={9, 4, 6, 5, 3, 2, 1, 7, 8};
int num=sizeof(num_list)/sizeof(num_list[0]);
BaseSorter instance;
/*bubble sort*/
instance = bubble_sorter_new(num_list, num);
printf("before sort\n");
instance->show(instance);
instance->sort(instance);
printf("after sort\n");
instance->show(instance);
bubble_sorter_free(instance);
/*quick sort*/
instance = quick_sorter_new(num_list, num);
printf("before sort\n");
instance->show(instance);
instance->sort(instance);
printf("after sort\n");
instance->show(instance);
quick_sorter_free(instance);
return 0;
}
実行結果です。
$ ./sample
before sort
[bubble_sorter]
9 4 6 5 3 2 1 7 8
swap 9 and 4 at 0
swap 9 and 6 at 1
...
after sort
[bubble_sorter]
1 2 3 4 5 6 7 8 9
before sort
[quick_sorter]
9 4 6 5 3 2 1 7 8
...
after sort
[quick_sorter]
1 2 3 4 5 6 7 8 9
ソート機能もしっかり動いていそうですね。
拡張時の設計を考える。継承しなくてもいいものは切り離して集約させればいいよね
こちらもTECHSCORE(テックスコア)さんのソートクラス拡張について考えます。intへのソートだけじゃなくtimespecへのソートも行いたいとなりました。
今の構成でも実装は出来ますが、実質同じものであるバブルソート・クイックソートの処理を丸々コピーして作り直さないといけない。
今後も同じようにソートはそのまま、対象が変わるという方針なら、出来るだけコアなロジックは切り離したい。というわけで以下のようにソート処理と実体を分離した構成にしてみます。
コードはこちら。まずはクラス定義の紹介です。
BaseSorterがSortImpleクラスに該当、BaseClassがSorterクラスに該当します。
struct BaseSorter_t;
typedef struct BaseSorter_t *BaseSorter;
struct BaseClass_t;
typedef struct BaseClass_t BaseClass_t, *BaseClass;
/*ソートクラス*/
struct BaseSorter_t {
/*ソート対象が必要なので、引数はベースクラス*/
void (*sort)(BaseClass baseinfo);
};
#define BASE_SORTER \
void (*sort)(BaseClass baseinfo);
/*ソート処理を保持する実体。ソート対象もこのクラスが持つ。*/
struct BaseClass_t{
BaseSorter sorter;
/*ソート処理を実現する為に必要な処理を抽象化。ソート処理全てじゃなくこれらのメソッドを実装すればソートが利用できるようにする。*/
int (*get_size)(BaseClass this);
bool (*operator_large)(BaseClass this, int src, int dist);
void (*swap)(BaseClass this, int place1, int place2);
void (*show)(BaseClass this);
};
#define BASE_CLASS\
BaseSorter sorter;\
int (*get_size)(BaseClass this);\
bool (*operator_large)(BaseClass this, int src, int dist);\
void (*swap)(BaseClass this, int place1, int place2);\
void (*show)(BaseClass this);
/*intソートを行う為のクラス*/
BaseClass int_sorter_new(int *num_list, int num);
void int_sorter_free(BaseClass this);
/*timespecソートを行う為のクラス*/
BaseClass time_sorter_new(struct timespec *time_list, int num);
void time_sorter_free(BaseClass this);
ソートクラスの実体は、例えばこんな感じにBaseClassのメソッドを利用してソートを実現するようにします。
完全にBaseClassと切り離せると理想的ですが、抽象に依存する形ならまあ許されるかな。
static void bubble_sort(BaseClass baseinfo) {
int size = baseinfo->get_size(baseinfo);
int i, j;
for(i=0; i<size-1; i++) {
for(j=1; j < size - i; j++) {
if( baseinfo->operator_large(baseinfo, j, j-1) ) {
printf("\tbubble_sort switch %d and %d\n", j-1, j);
baseinfo->swap(baseinfo, j-1, j);
}
}
}
}
ここではソート対象のデータを持つのはBaseClassなので、ソートの為に必要な操作を提供してあげる必要があります。
じゃあ追加したtimespecソートを行う為のクラスをはどうかというと、こんな感じ。ソート処理を丸ごとコピーするよりはすっきりしてるかな。
/*実体定義*/
typedef struct TimeSorter_t {
BASE_CLASS;
struct timespec *time_list;
int num;
} *TimeSorter;
/*位置交換処理*/
static void time_swap(BaseClass baseinfo, int place1, int place2) {
TimeSorter instance = (TimeSorter)baseinfo;
struct timespec stash_val = instance->time_list[place1];
instance->time_list[place1] = instance->time_list[place2];
instance->time_list[place2] = stash_val;
}
/*比較処理( < かどうか)*/
static bool time_operator_large(BaseClass this, int src, int dist) {
TimeSorter instance = (TimeSorter)this;
if( instance->time_list[src].tv_sec < instance->time_list[dist].tv_sec ) return true;
if( instance->time_list[src].tv_sec > instance->time_list[dist].tv_sec ) return false;
return (instance->time_list[src].tv_nsec < instance->time_list[dist].tv_nsec );
}
/*リストサイズ取得*/
static int time_get_size(BaseClass this) {
TimeSorter instance = (TimeSorter)this;
return instance->num;
}
/*インスタンス生成*/
BaseClass time_sorter_new(struct timespec *time_list, int num) {
TimeSorter instance = malloc(sizeof(*instance) + sizeof(struct timespec)*num);
if(!instance) return NULL;
instance->time_list = (struct timespec *)(instance + 1);
/*ここでどのソート処理を使うのか決めるような形にしました。*/
instance->sorter = quick_sorter_new();
/*各処理の設定*/
instance->get_size = time_get_size;
instance->operator_large = time_operator_large;
instance->swap = time_swap;
instance->show = time_show;
memcpy(instance->time_list, time_list, sizeof(struct timespec)*num);
instance->num = num;
return (BaseClass)instance;
}
main処理はsortの実行方法がsorter->sortになっただけで変わらずすっきりしています。
int main() {
int num_list[]={9, 4, 6, 5, 3, 2, 1, 7, 8};
int num=sizeof(num_list)/sizeof(num_list[0]);
BaseClass instance;
/*int sort*/
instance = int_sorter_new(num_list, num);
printf("before sort\n");
instance->show(instance);
instance->sorter->sort(instance);
printf("after sort\n");
instance->show(instance);
int_sorter_free(instance);
/*timespec値設定*/
struct timespec time_list[MAXSIZE];
int i;
memset(time_list, 0, sizeof(time_list));
for(i = MAXSIZE-1; 0 <= i; i --) {
clock_gettime(CLOCK_REALTIME, &time_list[i]);
usleep(300000);
}
/*timespec sort*/
instance = time_sorter_new(time_list, MAXSIZE);
printf("before sort\n");
instance->show(instance);
instance->sorter->sort(instance);
printf("after sort\n");
instance->show(instance);
time_sorter_free(instance);
return 0;
}
実行結果もちゃんとソート出来ていますね。
$ ./sample
before sort
9 4 6 5 3 2 1 7 8
bubble_sort switch 0 and 1
swap 9 and 4, at 0 and 1
...
after sort
1 2 3 4 5 6 7 8 9
before sort
1540029466.779385072 1540029466.478909887 1540029466.178707561 1540029465.878402743 1540029465.578190821 1540029465.278016381 1540029464.977899799 1540029464.677647972 1540029464.377505761
sort left 0 and right 8
swap 1540029466.779385072 and 1540029464.377505761, at 0 and 8
...
after sort
1540029464.377505761 1540029464.677647972 1540029464.977899799 1540029465.278016381 1540029465.578190821 1540029465.878402743 1540029466.178707561 1540029466.478909887 1540029466.779385072
感想
実装する前は、デザインパターン適用後はもっとスパっと橋渡しのクラスで処理が切り離せて、既存処理はそこまで変えなくていいのかなと思ったりしました。
ただ実際作ってみると実体をどこで持つの?という話が出てくるため、どうしても既存処理にもそれなりに手を入れる必要がありますね。
最初から綺麗に切り分けが出来るなら拡張しやすいとてもいい設計思想ですが、実際にプロジェクトで使うとなった場合にはどのようにbridgeさせるインターフェースをまとめるかについてよく考える必要がありそうです。
ただこういった役割をしっかり考えることは設計する上でいいことなので、このパターンは頭の片隅に置いておき、「全部1クラスにまとめるの?全部継承させる必要ある?」と考えながら設計するといいのかなと思いました。