はじめに
4月にQiita Advent Calendarを知り、今年の企画に是非参加したいと思っていたのが叶ってワクワクしています。
Qiitaに投稿をはじめた今年4月頃から、C言語を使ったオブジェクト指向表現やデザインパターンについて計20本ほど記事を書いてきました。
ここではその総括としてオブジェクト指向とはなんなのか?をC言語から考えてみたいと思います。
記事が長いわ!まとめて!という方は表題だけでも見てってください。
なぜC言語で?
本題の前にまずはなんでオブジェクト指向言語でないCでこんなことをしているかの説明をしておきます。
- 単純に私の中で一番言語利用歴が長い
- githubにコードを残しておきたかったので、いい題材になると考えた
- 実務を踏まえて考えられるので、オブジェクト指向の何がいいのかメリットを見定められる
- オブジェクト指向言語でなくても、いいものなら実はもううまく活用していたりするはず。そんな考えを確かめたかったから
実際Qiitaに記事をまとめてみて、自分なりにこういったものが実務に活かせている実感はあり、投稿して良かったと思っています。
では本題に参ります!
オブジェクト指向とは?
2行でまとめると以下のような感じかと。
- オブジェクトというものを通してシステムを考えること
- 実際に扱う際には「オブジェクト指向の3大要素」について考えるといい
本記事では、この「オブジェクト」、「オブジェクト指向の3大要素」が何かを通して、オブジェクト指向とは?を考えていきたいと思います。
C言語の話が出てくるのは「オブジェクト指向の3大要素」だけですが、「オブジェクト」に触れないわけにもいかないので、一部C言語と関係ない話になります。ご了承ください。
オブジェクトとは?
オブジェクトは「現実のものをそのまま捉えたもの」なのか?⇒時にはそうだけど常にそうではない
よくオブジェクトは「現実の物をそのまま捉えたもの」と説明されることがあります。これは端的な表現としてわかりやすいですが、ただオブジェクト指向を適用する相手はシステムです。
システムでは、扱う対象が「現実にある物」と直結することもあれば、データのような「抽象的なもの」もあります(というかこちらの方が多い)。
システム自体が「現実の物をそのまま捉えているわけではない」ので、現実世界で全てを語るのは難しいかなと思います。
オブジェクトは「システム内で扱う対象をわかりやすくグループ分けしたもの」とみるのはどうか?
「現実の物をそのまま捉えたもの」でオブジェクトが説明し切れない理由は、適用対象が現実の物だけで説明できないシステムだから。
ならシステムに対して似たような考え方を適用してあげればいいってことですよね。言い換えましょう!
前提が違うので言い回しも少し変えます。やりたいことはシステムをわかりやすく表現することなので、こんな感じかな。
- 「システム内で扱う物、データ全てをみんながわかりやすい形で表現したもの」
もう少し踏み込んで。じゃあわかりやすい形とは?これは簡単に言うと「ものをグループ分けすること」。このグループ1つがオブジェクトであり、クラス(メソッド+メンバー(データ)の集まり)であると考えています。
オブジェクトとは「システム内で扱う対象全てをわかりやすく(クラスという表現で)グループ分けしたもの」。こんな表現でいかがでしょうか?
オブジェクト指向三大要素とは?
「カプセル化」、「インターフェース」(「継承」、「ポリモーフィズム」)とカテゴリ分けして紹介
オブジェクト指向三大要素「カプセル化」、「継承」、「ポリモーフィズム」についてです。
C言語から考えてみた結果、こんな風にカテゴリ分けするのが個人的にはしっくりきました。
- カプセル化
- インターフェース
- 継承
- ポリモーフィズム
カプセル化 ~余計な情報はシャットアウト!グループ分けに強い、みんなに優しい取り組み
カプセル化。一言でいうとオブジェクト内のデータを隠ぺいすること。
カプセル化を考えることにはこんなメリットがあるように感じます。
- オブジェクトの役割と、オブジェクトを扱う方法(メソッド)だけきっちり定義すればいいので、適切なグループ分けに繋がる。
- 逆に役割決めがキーなので、ここに頭を使う必要があると思います。「名前付けが大事」と言われるのも役割決めに繋がるからだという印象
- オブジェクトが内部でどんな複雑なものを扱っていても、周りがその複雑さを知る必要がないので周りに優しい。
- オブジェクト自身もカプセル化されたデータを自由にいじれるので、自分に対しても優しい。
個人的にはカプセル化がオブジェクト指向の考えで一番扱いやすくメリットも大きい考え方だと思う。(図は適当)
C言語では、前方宣言を利用した簡単に情報の隠ぺいと、ポインタによる自由度の高いデータ参照が出来るので、カプセル化の考えを活かすことが出来ます!
例えばこのような形。struct capsule_class_t
の定義をヘッダーでしなくてもプログラムが正しく動作します。
struct capsule_class_t;
typedef struct capsule_class_t * CapsuleClass;
/*インスタンス生成API*/
CapsuleClass capsule_class_new(void);
/*メソッド定義*/
void capsule_class_method1(CapsuleClass this);
/*...*/
/*インスタンス解放API*/
void capsule_class_free(CapsuleClass this);
実際の定義はこのようにCファイル内で行います。Cファイル内で情報を完全に隠ぺい。
#include "capsule_class.h"
/*実際の定義はCファイル内に定義すればOK*/
struct capsule_class_t {
int member1;
int member2;
/*...*/
};
CapsuleClass capsule_class_new(void) {
CapsuleClass instance = calloc(1, sizeof(*instance));
/*初期化処理*/
return instance;
}
/*メソッド定義*/
void capsule_class_method1(CapsuleClass this) {
/*処理*/
}
/*...*/
/*インスタンス解放API*/
void capsule_class_free(CapsuleClass this) {
/*解放処理*/
free(this);
}
これをやるだけで構造がすっきりするので、個人的には凄いオススメです。
インターフェース ~実体は気にせず役割でメソッドをグループ分けする、賢い取り組み(抽象化)
カプセル化でデータ構造がすっきりしました。そうすると今度は同じような役割のメソッドが気になってくると思います。出来れば似たような役割のものはまとめたいですよね。
そこで登場するのがインターフェース。わかりやすい例では動画・音声等のプレイヤーに対するplay/pause/stopでしょうか。
再生したいものが動画でも音楽でも映画でも、操作の役割としては開始⇒play、停止⇒stopでまとめることが出来ます。
実体によって実際に操作時に行う振る舞いは違うけど、操作として同じものはザクっとグループ分けだ!綺麗に出来ればインターフェースによる抽象化の完了です!
C言語で表現すると構造体に関数ポインタを並べただけ。簡単なだけにC開発者の方でよく使ってるって方は沢山いると思います。
struct product_t;
typedef struct product_t *Product;
struct product_t {
/*メソッドだけを並べたクラスを定義。メソッド⇒関数ポインタ、クラス⇒構造体で表現。関数ポインタに実体を設定することで処理を実現*/
void (*show_name)(Product this);
void (*do_action)(Product this);
void (*other)(Product this);
};
さあ操作も役割毎に整理ができそうになってきました。それでは実際に使ってみましょう!
継承 ~インターフェースを引き継ぎ、ポリモーフィズムを活用する為の仕組み
インターフェースの実現には継承を使います。本来の継承はもっと色々な用途に利用することが出来ると思いますが、個人的に一番継承を活用出来るのがインターフェースを利用した場合かなと思っています。なので限定した表題にしました。
C言語での継承・インターフェースクラスの実現は例えばこんな風に出来ます。カプセル化でメソッドの実体は隠ぺいし、Cファイル内部で関数ポインタに処理を代入するだけ。
struct product_t;
typedef struct product_t *Product;
struct product_t {
/*メソッドだけを並べたクラスを定義。メソッド⇒関数ポインタ、クラス⇒構造体で表現。関数ポインタに実体を設定することで処理を実現*/
void (*show_name)(Product this);
void (*do_action)(Product this);
void (*other)(Product this);
};
/*インターフェースメソッドと同じ関数ポインタをマクロ定義*/
#define PRODUCT_IF \
void (*show_name)(Product this);\
void (*do_action)(Product this);\
void (*other)(Product this);
/*実体生成用API*/
Product product_new();
void product_free(Product this);
struct product_impl_t {
/*先頭にマクロを記載*/
PRODUCT_IF
/*メンバーがいるならメンバーをマクロの下に定義*/
}
void show_name_impl(Product this) {
/*処理を記載*/
}
void do_action_impl(Product this) {
/*処理を記載*/
}
void other_impl(Product this) {
/*処理を記載*/
}
/*以降エラー処理は省略*/
Product product_new() {
Product instance = calloc(1, sizeof(*instance));
instance->show_name = show_name_impl;
instance->do_action = do_action_impl;
instance->other = other_impl;
/*他メンバーの初期化*/
return instance;
}
void product_free(Product this) {
free(this);
}
多重継承は見にくくて実用的でないので割愛。
ポリモーフィズム ~インターフェースのメソッド実体をクラス毎に変え、操作のバリエーションを豊かに
継承を利用しインターフェースのメソッドに色々な操作を与えることで、生成したクラス毎に振る舞いを変えることが出来るのがポリモーフィズムです。
操作方法はインターフェースで定義した使い方と同じなので、生成した実体を変えるだけで、利用者に優しく振る舞いを変えることが出来る粋なやつ。
C言語での実現方法はというと、ただ関数ポインタを差し替えるだけです。
static void show_carname_toyota(Product this) {
printf("Toyota:prius\n");
}
static void show_carname_nissan(Product this) {
printf("Nissan:leaf\n");
}
/*生成時に実体を設定すれば、周りから見ると同じ操作なのに振る舞いが変わる!便利!*/
static Product toyota_factory(void) {
Product instance = calloc(1, sizeof(*instance));
instance->show_name = show_carname_toyota;
/*...*/
return instance;
}
static Product nissan_factory(void) {
Productinstance = calloc(1, sizeof(*instance));
instance->show_name = show_carname_nissan;
/*...*/
return instance;
}
C言語での表現はただの関数ポインタ差し替えなので、同一クラス内で動的にメソッドを差し替えるなんてことも簡単に出来ます。便利!
typedef enum {
TOYOTA,
NISSAN,
} CAR_TYPE_E;
/*同じ定義なので省略*/
/*type指定により動的にメソッド実体の差し替えを行う*/
static void switch_cartype(Product this, CAR_TYPE_E type) {
if(type == TOYOTA) this->show_name = show_carname_toyota;
else this->show_name = show_carname_nissan;
}
インターフェースによるポリモーフィズム、いつ使うと便利か?⇒例えば実体を全く意識しなくてもいい優しい世界を作る時
Cでは関数ポインタ差し替えが出来るので、わざわざポリモーフィズムに倣わなくても振る舞いが変えることが出来ます。それでもオブジェクト指向のインターフェース継承によるポリモーフィズムを使う方がメリットの大きいケースがあります。それは以下のような場合。
- インターフェイスを実際に使用する側が、その実体を意識しなくていいような構成にする場合
先ほどの例でいうと、インターフェースを利用する側がtoyota_factoryなのかnissan_factoryなのか、どの実体を生成するか選ぶ必要があります。
せっかくメソッドを抽象化して実体を隠したのに、その実体を使用者が知らないといけないのはもったいない、どうせなら実体も隠ぺいしたい!インターフェースを上手に定義すれば、そんな思いを実現することも出来ます。
(抽象に依存するってやつですかね)
具体例は例えばFactory Methodパターン。全コードを書いて貼ると長くなるので一部抜粋を抜粋。
このshow_nameを差し替えれば振る舞いが変わるし、product_factoryを差し替えればインターフェースの実体も変えることが出来ます。
//継承も出来る
typedef struct car_t {
PRODUCT_CLASS
char * name;
char * grade;
} *Car;
static Product toyota_factory(void) {
Car instance = calloc(1, sizeof(*instance));
instance->name = "prius";
instance->grade = "Apremium";
/*ここを差し替えることで振る舞いも変更可能*/
instance->show_name = show_carname;
return (Product)instance;
}
static void toyota_factory_free(Product this) {
free(this);
}
Factory car_factory_new() {
Factory instance = calloc(1, sizeof(*instance));
/*生成用のインターフェースクラスを定義して、生成メソッドにfactoryを指定。ここをnissan_factoryにすれば日産車クラスが生成されるようになる。*/
instance->product_factory = toyota_factory;
instance->product_free = toyota_factory_free;
return instance;
}
個人的には実体を隠ぺいしたい!という思いが強かったのでこの発想は凄くメリットあるな!と感じています。
(Abstruct Factoryパターンを使うと更に抽象化が加速しますが、私はあまり使いこなせる自信がない)
後は登録されている情報を全てなめるというような、実体が動的に変わるケースならget_all_if_instance
みたいな実体をまとめて取得するメソッドを持つ管理クラスを用意するなんてのもいいかなと思います。
最後に~オブジェクト指向は便利な道具。振り回されるのではなく自分のいいように活用しよう
というわけで、C言語から考えるオブジェクト指向について書かせていただきました。
オブジェクト指向言語であるかどうか関係なく、オブジェクト指向で考えることのメリットが少しでも伝われば幸いです。
また、今回一番最初に「いいものなら実はもううまく活用していたりするはず」と書きました。
何故これをこの記事でも書いたかというと、結局オブジェクト指向もいいシステムを作るためのただの道具ということも伝えたかったからです。
オブジェクト指向という道具が便利だと思うなら、オブジェクト指向言語かどうか関係なくうまく取り入れればいいし、多分意識せずに使っている部分もあると思います。
逆にこの道具の中で必要のないものがあるならそれを使わないという選択をしても別にいい。どうやって道具を使うかの考え方を押し付けず、囚われず、自分達にとってベストな形で活用すればいいのかなと思います。
参考
オブジェクト指向を知るために参考にした記事は沢山ありますが、特に個人的に参考になった記事はこちら:
オブジェクト指向と10年戦ってわかったこと