0/2 はじめに void *
1/2 クラス表現と継承
1/2 2018/07/01 再考しました 再考:クラス表現と継承
2/2 インターフェイス, オーバーライド ←ここ
今回はインターフェイスクラスとオーバーライドです。
正直インターフェイスクラスの実現を書いた後に、
今回記載する好みの話を書きたくてオブジェクト指向の深堀をしてる部分があります。
C言語、インターフェイスクラスの概念や実装方法はよく見かけるやつだ!って印象です。
インターフェイスクラスとは
メソッドだけ定義しておいて、各自にその実装クラスを任せる。
使い手は実装クラスの選択をすることで振る舞いが変わるという便利なものです。
C言語の言葉でざっくり書くと、
インターフェイスクラス⇒動的ライブラリのAPI定義
実装クラス⇒動的ライブラリの本体
ですね。複数持てる点が異なりますが。
C言語でのインターフェイスクラス表現の有用性について
実現方法としては、前回のpublicメソッドの表現で書いたので省きますが、
そこで記載したとおり、
Cでクラスメソッドを呼ぶ際、this
を使うなら引数で渡さないといけない
と書きました。
そうなると、沢山インターフェイスクラスの実装クラスを実装することを考えた場合、
そのAPIを公開APIで表現するか関数ポインタでインターフェイスクラスらしく表現するかは、かなり好みが分かれると思います。
単純にインターフェイスクラスで実現できるから実現しましょう!
といっても、既存の仕組みと変わらないんじゃもったいないですよね。
じゃあどういったケースが有効なんでしょう?
ケース1: インターフェイスを実際に使用する側が実体を全く意識しなくていい場合
例えば同じインターフェイスのモジュールが追加されたり削除されたりするシステムでは、使い手にインターフェイス実装クラスを意識させず、管理クラスを作ったりする場合があると思います。
こんな時は、C言語でインターフェイスクラスを導入する意味が出てきます。
インターフェイスクラスの実現で書いた例もそんな感じです。
インタビューアーがインタビューを受ける人たちの情報をインターフェイスクラスとして受け取り、順々に決まった質問をしていく。
インタビューアーは質問事項に回答してもらえればいいので、インタビューを受ける人については別の人に管理を任せればいい感じです。
コードを整理し、解説。まずはculture_ifというインターフェイスクラスを定義。
struct culture_if {
char *(*introduce)();//自己紹介
char *(*get_name)();//名前
char *(*answer)(int id);//質問
};
次にpeople_manager
クラスというpublic API
を定義します。
#include "culture_if"
void * people_manager_new();
int people_manager_get_any_people(void *, struct culture_if *peoples);
void * people_manager_free(void *);
people_manager
がnewされた際に今日来られた方たちのculture_if
実装クラスをnew
し、
people_manager_get_any_people
で一覧を渡す仕組みにします。
で、インタビューアーの実装がこんな感じ。
#include <stdio.h>
#include "people_manager.h"
#include "culture_if"
int main() {
struct {
int id;
char * question;
} questions[] =
{
{FOOD, FOOD_QUESTION},
{HAPPY, HAPPY_QUESTION},
{CONPUTER, CONPUTER_QUESTION},
};
struct culture_if max_members[10];
void * people_manager = people_manager_new();
int member_num = people_manager_get_any_people(max_members);
printf("[インタビューアー] 皆さんこんにちは。本日はよろしくお願いします。まずは軽く自己紹介をお願いします。\n");
int i=0;
for( i = 0 ; i < member_num; i ++ ) {
printf(" [%d番目の方の自己紹介]\n", i+1);
printf("\t%s\n", max_members[i].introduce() );
}
printf("\n");
printf("[インタビューアー] ありがとうございます。それではこれから順次質問をしていきたいと思います。\n気軽にお答えください。\n\n");
int j=0;
for ( i = 0 ; i < sizeof(questions)/sizeof(questions[0]); i ++ ) {
printf("Q %d: %s\n", i+1, questions[i].question);
for( j = 0 ; j < member_num ; j ++ ) {
printf(" [A:%sさん]\n", max_members[i].get_name());
printf("\t%s\n", max_members[i].answer(i));
}
printf("\n");
}
printf("[インタビューアー]以上で質問は終わりです。皆さんありがとうございました!\n");
people_manager_free(people_manager);
return 0;
}
これで、インタビューを受ける人が変わってもインタビューアー側の実装そのままでいけますね。
こういった使い方はいいんじゃないでしょうか。
ケース2: dlopenによる静的ライブラリのプラグイン追加
Linuxの場合だけかな?標準関数にdlopen/dlsymというものがあります。
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
void *dlsym(void *handle, const char *symbol);
manpageより抜粋。
- 関数 dlopen() は、ヌル終端された文字列 filename で指定されたファイル名の動的ライブラリ (dynamic library) をロードし、 その動的ライブラリへの内部「ハンドル」を返す。
- 関数 dlsym() は、 dlopen() が返した動的ライブラリの「ハンドル」と、 NULL 終端されたシンボル名の文字列を引き数に取り、 そのシンボルがロードされたメモリーのアドレスを返す。
要はdlopenでライブラリのファイルを指定して、その後dlsymで関数名を指定すれば、関数と対応した関数ポインタがもらえる。以降関数ポインタを使ってライブラリの関数が実行できる。
既存の仕組みですが、まさしくインターフェイスクラスの考え方なんですよね。
実際にインターフェイス実装クラスとしての使い方はこんな感じ
- インターフェイスクラスとなる関数ポインタを定義する。
culture.h
で行きましょう。 - インターフェイスクラスの各メソッドと対応するAPI名を決める。
- 上記3APIを実装したライブラリを
dlopen
で読み込み、dlsym
で対応するculture_if
に設定する。
例えば以下のように名前を決めて、インターフェイス実装の担当者に各APIを実装し、ライブラリにしてもらいましょう。
-
void (*introduce)()
⇒void culture_if_introduce();
//自己紹介 -
char *(*get_name)()
⇒void culture_if_get_name();
//名前 -
char *(*answer)(int id)
⇒void culture_if_answer();
//質問
出来たらdlopenで各ライブラリを読み込めば、インターフェイス実装クラスのメソッド達が完成です。
struct culture_if * culture_if_new( chhar * lname /*"ライブラリファイル名"*/) {
void * handle = dlopen(lname , flag);
struct culture_if * culture_if = malloc(sizeof(struct culture_if ));
culture_if->introduce = dlsym(handle, "culture_if_introduce");
culture_if->get_name= dlsym(handle, "culture_if_get_name");
culture_if->answer= dlsym(handle, "culture_if_answer");
return culture_if;
}
これのいいところは、実装クラス側のメソッド名も固定出来るのと、管理側のロジックも決まっているところなんですよね。
うまくやれば管理側に手を入れずにインターフェイス実装クラスを増やすことが出来ます。
多分よくあるプラグイン追加型のOSSはこんな感じなんじゃないすかね。
これ、絶対なにかのデザインパターンにはまっているのでそのうちサンプルを作ると思います。
C言語でのインターフェイスクラス表現 まとめ
- 関数ポインタを自由に扱えるC言語にとって、インターフェイスクラス表現は使いどころの沢山ある有効な手段
- 何も考えずに適用するのは、既存のCの仕組みに対する優位性がない。
- クラスに限らず、インターフェイスっていいよね。
オーバーライド
C言語では関数ポインタを使ってインターフェイスを表現していました。
ということはオーバーライドも超簡単。ポインタを上書きするだけです。
ということは、オーバーライドを意識してpublic method
を定義するなら、関数ポインタにした方がいいですね。
上書きすればいいだけなのでサンプルは省略。
感想
その0~2で長々と書いていきた所感です。
- Cでのクラス設計適用はコードの見やすさにもつながりとても良い。実装表現は好みの問題
- Cでのインターフェイスクラス設計はとても有用。というか意識しなくともすでに使っているはず。
- クラス継承については苦手。集約の形で表現した方がいいのでは?
得意不得意ありますし、オブジェクト指向言語そのものの書き方にはならない部分はありますが、
代案の表現方法も用いてあげれば、ちゃんとC言語でもオブジェクト指向設計は出来る!
というのが私の感想です。
長くなりましたが以上です。