はじめに
CodeZineではお初にお目にかかります、επιστημη(エピステーメー)です。最初のアーティクルはクラシックなCのお話。
昨今のアプリケーションはオブジェクト指向言語による実装が主流と言ってもいいでしょう。C++、Java、VB.NETさらにはRubyやPythonといったスクリプト言語まで、オブジェクト指向でない言語を探すのに苦労するくらいです。
本記事では、今なお現役バリバリで活躍している手続き型言語の代表格(?)Cによる、オブジェクト指向のマネゴト(オブジェクト指向風味のCコーディングスタイル)を試みます。
対象読者
もっぱらCを主な開発言語として使ってはいるけども、オブジェクト指向に興味と憧れを抱いている方。
抽象データ型
手始めにオブジェクト指向の特徴の1つ、「抽象データ型(ADT:Abstract Data Type)」をCで実現してみます。抽象データ型とは、データの具体的な詳細に一切触れず、そのデータに対して可能な操作の集合でデータを定義するものです。
例えば簡単な例、「自動車の燃費シミュレーション」を考えてみましょう。車(Car)の内部にはガソリンタンクがあり、その容量(capacity)とガソリンタンクに蓄えられたガソリンの残量(remain)をデータとして持っています。車を走らすとおおむねその距離に応じてガソリンを消費しつつ、走行距離を伸ばしていきます。
ここまでを構造体と関数で表現すると、次のようになります。
#ifndef CAR_H__ #define CAR_H__ struct Car_type { int capacity_; /* タンク容量[L] */ int remain_; /* ガソリン残量[L] */ int mileage_; /* 燃費[L/km] */ int distance_; /* 走行距離[km] */ }; typedef Car_type* Car; void car_initialize(Car car, int capacity, int mileage); void car_drive(Car car, int distance); #endif
以下、関数car_initialize
とcar_drive
はそれぞれ「与えられたタンク容量と燃費で車を初期化する」「与えられた距離(km)走行する」を行います。
#include "Car.h" void car_initialize(Car car, int capacity, int mileage) { car->capacity_ = capacity; car->mileage_ = mileage; car->remain_ = 0; /* ガス欠状態 */ car->distance_ = 0; /* メータ・クリア */ } void car_drive(Car car, int distance) { /* ガソリン残量以上には走れない… */ while ( car->mileage_ * distance > car->remain_ ) { --distance; } car->distance_ += distance; car->remain_ -= car->mileage_ * distance; }
これらをmain
関数から呼び出してみます。
#include <stdio.h> #include "Car.h" int main() { Car_type c; car_initialize(&c, 50, 2); c.remain_ = 40; /* 給油 */ car_drive(&c, 5); /* 軽くドライブ */ car_drive(&c, 2); /* さらにも少し */ printf("%d[km], remain: %d[L]\n", c.distance_, c.remain_); return 0; }
実にオーソドックスです。程度の差こそあれ、おそらく多くのCアプリケーションはこれと似たコードで実装されていることでしょう。
安全性に問題のある車
ここでいくつかの問題(不安要素)があります。1つはCar_type
の利用者(ここでは「simulate.c」の作者)はCar_type
が持つメンバの型と名前を知らないことには走行距離やガソリン残量をプリントできません。
メンバの型と名前を知るということは、すなわちそれらメンバを勝手に書き換えることができるということです。そのため例えばタンク容量を超えて給油したり、ヨソ様の車からガソリンを失敬したりすることができてしまいます。さらに、車の設計者(「Car.h」/「Car.c」の設計/実装者)がメンバの名前/型を変更するたびに利用者はコードの変更を余儀なくされます。車を修理に出すたびに操作法が変わってしまうわけです。
利用者が必要なCar_type
に対するすべての操作、ここでは給油や走行距離の取得などを関数で提供すれば、利用者はCar_type
の詳細を知ることも触れることもなく、提供された関数を使ってシミュレーションが可能です。
ところが、利用者には依然としてCar_type
の中身が丸見え(アクセス可能)ですから、用意された関数を呼ばずとも直接メンバを参照/変更できてしまいます。利用者つまり「Car.h」を#include
したコードにCar_type
の中身を隠すことはできないものでしょうか?