インターフェイスというのはメソッドのことです。と言い切ってしまうと語弊があるのですが、C++ではクラスのインターフェイスをメソッドで表すことになっています。
インターフェイスというのはクラスを操作する手段です。オブジェクト指向の解説では、メッセージを送受信することでオブジェクト同士が協調して動作するというイメージがよく語られますが、C++の場合はメソッドをインターフェイス(窓口)としてメッセージを送受信するわけです。そのメソッドが呼び出されたという事実そのものもクラスにとってひとつのメッセージですし、メソッドに引数があればそれもまたメッセージとなります。
インターフェイスという概念は、データ隠蔽の概念の上に成り立っています。まずはデータの隠蔽がきちっとしていなければなりません。class CCounter
{
public:
m_nCount;
m_nMax;
CCounter(int max);
int Count();
bool IsEnd() const;
};
int CCounter::Count()
{
if (m_nCount < m_nMax)
if (++m_nCount >= m_nMax)
printf("count is over!n");
return m_nCount;
}bool CCounter::IsEnd() const
{
return m_nCount >= m_nMax;
}foo()
{
CCounter cnt(100);
cnt.m_nCount = 120;
while(cnt.IsEnd())
cnt.Count();
}
オブジェクト指向ではデータの隠蔽にとやかくうるさいわけですが、その理由は単純明快です。上記の例のように、クラスのメンバ変数に外部から簡単にアクセスできてしまうようではめちゃくちゃになってしまいます。"count is over!" が出力されないまま終了してしまいますよね。
もし隠蔽しなかった場合、そのときは都合が良くても、仕様変更したい場合や継承したい場合などにたいへん困ることになるでしょう。
インターフェイスはメンバ変数への単純なアクセスだけでなく、メンバ変数の内容を元に加工したデータをやりとりすることもあります。例えば、上記の CCounter で IsEnd メソッドは、別に終了を意味する bool のフラグをメンバ変数に従えているわけではありません。
メンバ変数が private であれば、カウントが確かに終了しているかどうかは実際にカウンターの値や最大値などの情報を記録している CCounter だけが判別できます。これは CCounter だけが判別すればよいというように、情報の一元管理を可能にします。このような考え方を、情報のカプセル化といいます。
隠蔽、カプセル化、インターフェイスの3つは全く別なものではなく、それぞれがお互いに関係しあっている概念です。
隠蔽とカプセル化がきちんとしているという前提があれば、クラスを設計するときはインターフェイスを考えるだけで済みます。具体的な実装をする前に、class の定義を作り、インターフェイス、すなわちメソッドを一通り揃えるというやりかたが一般的な設計の流れとなっています。
class CFile
{
private:
FILE m_Hdl;
public:
CFile(char *filename, char *mode);
virtual ~CFile();
int GetChar();
bool IsEof() const;
};
CFile::CFile(char *filename, char *mode)
{
m_Hdl = fopen(filename, mode);
assert(m_Hdl >= 0);
}CFile::~CFile()
{
fclose(m_Hdl);
}int CFile::GetChar()
{
return fgetc(m_Hdl);
}bool CFile::IsEof() const
{
return feof(m_Hdl);
}
CFile はファイルハンドルを隠蔽する、ありがちな例です。CFile をデストラクトすれば自動的に close されるという、たいへん有用な例と言えるでしょう。
※このクラスはある問題を含んでいますが、それは後日解説します。
int foo()
{
int cnt;
CFile file("test.dat", "rb");
while(!file.IsEof())
{
int c = file.GetChar();
if (c == 'r')
return cnt;
putc(c);
cnt++;
}
return cnt;
}
例えばこのように利用するわけです。Cでプログラムする場合は return の前に close しなければなりませんが、CFile を使えば自動的にファイルをクローズしてくれます。
int DispWithoutNumber (CFile &file)
{
int c;
while((c = file.GetChar()) != EOF)
{
if (!isnum(c))
putc(c);
}
}
DispWithoutNumber は、指定されたファイルから文字を読み込んで、数字以外を表示する関数です。この関数は、CFile を継承いているクラス全てが利用できるので、汎用性も高いでしょう。
さて、ここで特定のメモリ領域にあるデータを、同じように数字以外の文字を表示するようにしたい場合はどうしますか? メモリは fopen も fclose もできませんから、CFile は継承できません。
int DispWithoutNumberFromMemory(char *buff, int size);
こんな新しい関数を作らなければならなくなってしまいました。となると、CFile は本当に汎用性が高かったのでしょうか?
オブジェクト指向は再利用性の高さが売りだったはずですよね。もちろんC++でも、このような問題に対する解決方法はちゃんと用意されています。
class CStreamInterface
{
public:
virtual int GetChar() = 0;
virtual bool IsEof() const = 0;
};
class CFile : public CStreamInterface
{
private:
FILE m_Hdl;
public:
CFile(char *filename, char *mode);
virtual ~CFile();
virtual int GetChar();
virtual bool IsEof() const;
};
class CMemoryStream : public CStreamInterface
{
private:
int m_Seek, m_Size;
char *m_pBuff;
public:
CMemoryStream(char *buff, int size);
virtual ~CMemoryStream() {}
virtual int GetChar();
virtual bool IsEof() const;
};
CMemoryStream::CMemoryStream(char *buff, int size)
: m_Seek(0), m_Size(size), m_pBuff(buff)
{
}int CMemoryStream::GetChar()
{
if (IsEof())
return EOF;
return m_pBuff[m_Seek++];
}bool CMemoryStream::IsEof()
{
return m_Seek >= m_Size;
}
内部的にかなり性質が異なる場合でも、インターフェイスが同じ場合には、純粋仮想関数だけしかない抽象基底クラスを作成します。
純粋仮想関数というのは実装のない仮想関数で、純粋仮想関数を含むクラスを抽象基底クラスと呼びます。抽象基底クラスはインスタンス化できませんので、オブジェクトを生成するためには必ず継承しなければなりません。
同じ抽象基底クラスを継承させることで、異なるクラスに同じインターフェイスを与えることができます。以下の DispWithoutNumber 改良版ならば、上記の CFile も CMemoryStream も変更することなく扱えるのです。これで DispWithoutNumber をいくつも作るハメになるようなことは避けられました。
int DispWithoutNumber (CStreamInterface &stream)
{
while(!stream.IsEof())
{
int c = stream.GetChar();
if (!isdigit(c))
putc(c);
}
}
Java には純粋仮想関数のみで構成された抽象基底クラスと同様な仕組みを備えています。その名も interface です。
ならば全てのクラスで抽象基底クラスを導入すべきかというと、私は今のところそうとは言い切れないと思っています。C++の継承も、Java の interface も、慎重な判断と十分な経験が必要です。
C++にしても Java にしても、このあたりは何かと議論の種になりがちなところですがうまく使えば再利用性が高くなります。似たようなコードを何度も作っていたのでは、効率が悪いだけでなくつまらないバグを入れてしまう危険も増えます。みなさんもうまく使いこなして再利用性を高めてみて下さい。