オセロを例に考える

非オブジェクト指向的に考えると

既に書いたように、オブジェクト指向について説明するのに、 犬や猫が出てくるようなたとえ話は有害でしかないと思います。 やはり、現実のプログラミングに即した例題を使用するべきでしょう。

そこで、本稿ではまず「オセロゲーム」を例として説明を試みます。

Cプログラマにオセロゲームを作らせたら、 まず、盤面を以下のようなグローバル変数で管理しようとするのではないでしょうか。

#define BLANK_CELL 0  /* 何も置かれていない */
#define BLACK_CELL 1  /* 黒のコマが置かれている */
#define WHITE_CELL 2  /* 白のコマが置かれている */

/*
 * 「cell」にはExcelなどの「マス目」の意味がありますので、
 * ここでは、オセロの盤面のマス目を表現していると思ってください。
 */
int cell[8][8];

この例では、intの2次元配列を使用してオセロの盤面を表現しています。 何も置かれていなければ0、黒のコマが置かれていれば1、白のコマが置かれていれば2です。 そして、ソース中に0とか1とかをばらまかなくて済むように、#defineしています。 まあまともなCプログラマなら、intを#defineするのではなくenumを使うと思いますが、 ここでは、(まだ)列挙型のないJavaに合わせてintにしてあります。

さて、このようにすることで、 とにもかくにもオセロの盤面を表現することはできました。 しかし、多くの人は、「盤面をグローバル変数にしてしまうのはまずい」 と思ったのではないでしょうか。

盤面をグローバル変数にしてしまうと、 プログラムのどこからでも変更できてしまいます。 これでは、盤面を正しい状態に保つのは困難です。 もちろん、 わざと盤面をおかしくしようと思ってプログラムを書くわけではないですが、 プログラムが大きくなると、 どこからでも書き換えられるグローバル変数は、 なかなかうまく管理できるものではありません。

オセロの場合、ルール上、盤面に対してできる操作は限られています。 基本的には、白か黒の石を、「置ける場所」に置くことしかできません。 「置ける場所」とは、そこに石を置くことで、 相手の石をひっくり返すことができる場所のことです。

そこで、盤面をそのままグローバル変数にするのではなく、 盤面を操作する関数だけを公開し、外部からはその関数だけを用いて 盤面にアクセスする、という方法が考えられます。 具体的には以下のような関数を用意することになるでしょう。

typedef enum {
    TRUE = 1,
    FALSE = 0
} Boolean;

#define SENTE 1
#define GOTE 2

/**
 * x, yの箇所に、senteOrGote(SENTEまたはGOTE)の手番で石を置く。
 * 置けない箇所を指定したら致命的エラー。
 */
void board_put(int x, int y, int senteOrGote);

/**
 * x, yの箇所に指定した手番の石が置けるかどうかをチェックする。
 */
Boolean board_check(int x, int y, int senteOrGote);

/**
 * 盤面を初期化する。
 */
void board_initialize();

/**
 * x, yで指定されたマスの状態を返す。
 * 戻り値はBLANK_CELLまたはBLACK_CELLまたはWHITE_CELL。
 */
int board_get_cell_state(int x, int y);

これらの関数は、名前が全て「board_」で始まっています。 このような命名規則を適用することで、 他の部分との関数名の衝突を避けようとしているわけです。

board_put()関数で石を置くことができます。 石を置くことにより、挟んだ石は自動的にひっくり返されます。 board_put()では、置けない箇所に石を置いたら致命的エラーなので、 事前にboard_check()関数を使って そこに石が置けるかどうかをチェックしなければなりません。 board_initialize()を呼び出すと、 盤面の状態を初期状態(石が4つだけ置いてある状態)に戻すことができます。 つまり、ゲームを最初からやり直すことができるわけです。 board_get_cell_state()は、盤面の、x, yで指定されたマスの状態を返します。 盤面を画面に表示したりする際は、この関数を繰り返し呼び出すことになります。

さて、これで、「盤面」を操作する関数群を一通り用意することができました。 盤面のデータ(2次元配列cell)を操作する場合は、必ずこれらの 関数を使うことにします。

これを図にすると、以下のようになります。

カプセル化の図

2次元配列cellは「非公開のデータ」に該当します。 board_で始まる関数群は、「公開された関数群」です。 また、board_put()のような関数では、 石をひっくり返すのはそれなりに複雑な処理ですから、 下請け関数を作りたいところでしょう。 それが「非公開の関数群」に該当します。

Cでこのようなことを実現するのなら、staticを使うことになります。 Cでは、変数宣言の前にstaticを付けることで、 変数をそのソースファイル以外からは見えなくすることが可能です。

/* 盤面を表現する2次元配列 */
static int cell[8][8];

同様に、関数定義にもstaticを付けることができます。

よって、たとえばboard.cというソースファイルを用意し、 cellのような変数、および内部でしか使わない関数はstatic指定してやれば、 外部に対しては、それらを隠蔽することができます。 そして、外部に公開する関数だけstaticを付けずに定義し、 それらの関数群のプロトタイプ宣言を書いたヘッダファイルを用意すれば、 外部からは、board.cは、「オセロの盤」という機能を持ったひとつの 「モジュール」として使用できる、ということになるわけです。

さて、ここまでで、使用者に公開する必要のないデータや関数を、 使用者に対し隠蔽することができました。 もしこれを「カプセル化」と呼ぶのであれば、 これでカプセル化は実現できたということになります。

しかし、現状では、カプセル化は実現できていても、 オブジェクト指向にはなっていません。 なぜならここにはオブジェクトがいないからです。

ところで、オブジェクト指向における「クラス」という概念の説明として、 「クラスとは、データと、それに関係する処理をひとまとめにしたものだ」 という説明がなされることがよくありますが、 これもいささか雑すぎる説明だといえるでしょう。 この説明からするとboard.cもクラスということになってしまいますが、 board.cはあくまで「モジュール」であって「クラス」ではありません。

で、これで何が困るのか?

既に書いたように、board.cはオブジェクト指向とは言えません。 そして、多くのCプログラマには、 「この設計でいったい何がいけないのか?」と思えるのではないかと思います。

board.cはうまく部品化できていますから、たとえば UNIXで動かしていたオセロプログラムをWindowsに移植する、 という場合にも、board.cはこのまま使用することができます。 レトロなコマンドラインで動くオセロを作る場合にも使えるでしょう。 オセロのようなゲームでは、コンピュータの思考ルーチンを作る際、 「思考ルーチン同士で戦わせる」ということを行いますが、 その場合もboard.cは全く手を加えずに使用することができます。

また、なんらかの事情でboard.cの実装を変更する場合 ---まあ、このケースではあまりなさそうではありますが、 たとえばメモリ節約のためにcellをintではなくcharにしよう、 という場合にも、外部に公開している関数の仕様さえ変更しなければ、 自由に変更可能です。

でも、たとえば、ネットワーク対応の対戦オセロを作ろうとしたらどうでしょう。

ネットワーク対戦オセロでは、Webからアクセスすると、 世界中の誰かと対戦できるようにします。 このようなものを実現したければ、ひとつのサーバで 複数のオセロの盤面を管理しなければならないことでしょう ※1

しかし、現状のboard.cでは、ひとつの盤面にしか対応できません。 2次元配列cellがひとつしか存在できないからです。

マルチプルインスタンスへの道

オセロの盤面がひとつしか存在できないのでは、 ネットワーク対戦を考えたときに困ることは分かりました。

これを解決するには、「盤面」を、「最初からそこに、じっと、 静的に存在する」 モノと考えるのではなく、 「必要に応じて動的に生成する」モノと考える必要があります。

つまり、「盤面」の使用者は、以下のように「盤面を生成する」必要があるわけです。

  board = board_create();

ここで、board_create()の戻り値の型については、いくつかの案が考えられますが ※2、 典型的には、以下のような構造体を定義し、そのポインタを返すという方法を 取るでしょう。

typedef struct {
    int cell[8][8];
} Board;

board_create()の中では、この構造体のためのメモリを(malloc()などで) 確保するわけです。

そして、盤面に石を置いたりする際には、 それが「どの盤面に対する操作であるのか」を指定するために、 boardを引数に渡す必要が出てきます。 「盤面」がひとつしか存在しない状況では「board_put(x, y)」で良いでしょうが、 盤面が複数存在するのであれば、「board_put(board, x, y)」のようにしなければ、 どの盤面に石を置くのかを指定できません。

よって、盤面に石を置くboard_put()関数は、以下のようになります。

void board_put(Board *board, int x, int y, int senteOrGote);

この例では、第1引数で「どの盤面に対する操作なのか」を明示しています。

同様に、他の関数群は以下のようになるでしょう。

Boolean board_check(Board *board, int x, int y, int senteOrGote);
int board_get_cell_state(Board *board, int x, int y);

board_initialize()関数がなくなっていますが、 それは不要だからです。初期化された盤面が欲しければ、 board_create()を使って新たに生成すれば済むからです。

「複数盤面対応版」を図にすると、以下のようになります。

マルチプルインスタンスの図

図中に、「どのオブジェクトを操作するのかを第1引数で指定する」 とあります。つまり、この図の下部に3つあるのが、 オブジェクト指向で言うところの「オブジェクト」です。 「オブジェクト」は、「インスタンス」と呼ばれることもあります。

---つまり、これがオブジェクト指向です。

以前のboard.cというモジュールは、カプセル化は実現できていましたが、 静的にひとつしか存在しませんでした。 それが、必要に応じていくつも生成できるようになったものがオブジェクトです。 そして、このようにオブジェクトを必要な数だけ生成し、 それに付属した関数を呼び出しながら動作していくというプログラミングスタイルが、 「オブジェクト指向」であるわけです。

なお、Cでこのようなことをする場合、Board構造体を使用者に丸見えにしたのでは カプセル化になりませんから、不完全型を使って隠蔽します。 その方法については、C言語ヨタ話第3回、 モジュールと命名とヘッダファイルと」に書きました。

オブジェクト指向言語によるサポート

ここまで、Cで「オセロの盤面」を実現する方法について考えてきました。 そして、

を達成することができました。

さて、JavaやC++などのオブジェクト指向言語では、

board_put(board, x, y);

のようには書きません。

Javaなら

board.put(x, y);

のように書きます。ちなみにC++では

board->put(x, y);

です。

図にすると、以下のように変化するわけです。

つまり、第1引数で渡していたboardが関数名の左側に来ます。 これにより、関数が「Boardに属するもの」であることが識別できるため、 関数名がバッティングしないように「board_xxx」という名前を付けて 逃げる必要がなくなったため、それを外すことができます。

こんなの見た目が変わっただけじゃないか!

全くそのとおり。継承を考えない限りにおいては、 オブジェクト指向言語といってもその程度のものです。

ちなみに、Cでは「関数」と呼ばれていたものは、 見た目が変わった途端に「メソッド」と名前を変えます。 だからどうだってほどのことでもないのですが。

また、C版では「board_create()」という関数がありましたが、 これはオブジェクト指向言語で言うところの「コンストラクタ」に相当します。

board = board_create();

と書く代わりに、

board = new Board();

と書くようになるわけです。

具体的なクラスの定義方法は、言語の入門書を参照してください。

まとめ



ひとつ上のページに戻る | ひとつ前のページ | ひとつ後のページ | トップページに戻る
mailto:PXU00211@nifty.ne.jp

このページに対してご意見・ご質問・ご感想等をいただいた場合、 公開することがあります。