|
C++で書くインターフェースの話
最近、プログラミングの概念として、 インターフェースについてあれこれと考えるようになりまして、 すこし、自分の頭の中を整理する意味で、このページを書いています。
ですから、このページを読む時に「勉強になる」とか「メリットがある」などとは
考えないでください。
主に、私が頭の中で考えたことをだらだらと書きだしているだけです。
ただ、プログラミングのさいの何らかのヒントになれば幸いと思っています。
いきなりですが、インターフェースをコーディングするさいの 書き方について取り上げてみます。まずは、C++でインターフェースを記述するにはどんな書き方があるのか、
思いつくまま列挙してみます。
パラメータと継承を使う おそらく一番オーソドックスなタイプと思われます。
オリジナルのインターフェースを継承して、新しいインターフェースをつくり、
実行時にパラメータとして渡す方法。
// 継承 inherit + パラメータ param
#include < stdio.h > // printf(), getchar()
// インターフェース(実装付き)
class OrgInterface
{
public:
virtual void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
virtual void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
} ;
// ボディ(OrgInterface を利用して実装されている)
class OrgBody
{
public:
void CallMethod(
OrgInterface & in_param // 引数 param
)
{
in_param.Method1() ;
in_param.Method2() ;
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
virtual void Method1()
{
puts( "NewInterface1::Method1()" ) ;
}
// 新しいメソッドに置き換える
public:
virtual void Method2()
{
puts( "NewInterface1::Method2()" ) ;
}
} ;
class NewInterface2 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
virtual void Method1()
{
puts( "NewInterface2::Method1()" ) ;
}
// オリジナルのメソッドを使う(置き換えない)
// public: virtual void Method2()
// {
// puts( "NewInterface2::Method2()" ) ;
// }
} ;
//---------------------------
// 実装
int main( void )
{
// 新しいインターフェースの実体を作る。
NewInterface1 new_interface1 ;
NewInterface2 new_interface2 ;
// ボディの実体を作る。
OrgBody org_body ;
// パラメータでインターフェースを渡すので、
// 1つのボディを2種類のインターフェースで実行できる。
org_body.CallMethod( new_interface1 ) ;
org_body.CallMethod( new_interface2 ) ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewInterface1::Method1()
NewInterface1::Method2()
NewInterface2::Method1()
OrgInterface::Method2()
*/
|
パラメータとテンプレートを使う方法。 その1の方法では、継承を使ってますが、
継承の代わりにテンプレート関数を使っての実装方法です。 パラメータとして渡すのは同じですが、
継承と違ってオリジナルのインターフェースを用意する必要がありません。 それが、メリットなのか?デメリットなのか?、いまのところなんとも言えません。
各自で考えてほしいです。
// パラメータparameta + テンプレートtemplate
#include < stdio.h > // printf(), getchar()
/* インターフェースの記述は必要ない
class OrgInterface
{
public:
void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
};
*/
// ボディ( _T を利用して実装されている)
class OrgBody
{
public:
template< class _T > // テンプレート関数
void CallMethod( _T & in_param ) // パラメータ
{
in_param.Method1() ;
in_param.Method2() ;
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1
{
// メソッドの実装(置き換えるという考え方ではない)
public:
void Method1()
{
puts( "NewInterface1::Method1()" ) ;
}
public:
void Method2()
{
puts( "NewInterface1::Method2()" ) ;
}
} ;
class NewInterface2
{
// メソッドの実装(置き換えるという考え方ではない)
public:
void Method1()
{
puts( "NewInterface2::Method1()" ) ;
}
public:
void Method2()
{
puts( "NewInterface2::Method2()" ) ;
}
} ;
//---------------------------
// 実装
int main( void )
{
// 新しいインターフェースの実体を作る。
NewInterface1 new_interface1 ;
NewInterface2 new_interface2 ;
// ボディの実体を作る。
OrgBody org_body ;
// パラメータでインターフェースを渡すので、
// 1つのボディを2種類のインターフェースで実行できる。
org_body.CallMethod( new_interface1 ) ;
org_body.CallMethod( new_interface2 ) ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewInterface1::Method1()
NewInterface1::Method2()
NewInterface2::Method1()
NewInterface2::Method2()
*/
|
今度は、パラメータ、テンプレート、継承、すべてを使う方法です。 その1と、その2の
合わせ技のようなモノです。 オリジナルのインターフェースは用意されていますが、
継承して使っても使わなくてもどちらの使い方でもできるメリットがあります。
// パラメータ parameta + テンプレート template + 継承 inherit
#include < stdio.h > // printf(), getchar()
// インターフェース(実装付き)
class OrgInterface
{
public:
void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
};
// ボディ( _T を利用して実装されている)
class OrgBody
{
public:
template< class _T > // テンプレート関数
void CallMethod( _T & in_param )
{
in_param.Method1() ;
in_param.Method2() ;
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody1::Method1()" ) ;
}
// 新しいメソッドに置き換える
public:
void Method2()
{
puts( "NewBody1::Method2()" ) ;
}
} ;
class NewInterface2 : public OrgInterface
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody2::Method1()" ) ;
}
// オリジナルのメソッドを使う(置き換えない)
// public: void Method2()
// {
// puts( "NewBody2::Method2()" ) ;
// }
};
//---------------------------
// 実装
int main( void )
{
// 新しいインターフェースの実体を作る。
NewInterface1 new_interface1 ;
NewInterface2 new_interface2 ;
// ボディの実体を作る。
OrgBody org_body ;
// パラメータでインターフェースを渡すので、
// 1つのボディを2種類のインターフェースで実行できる。
org_body.CallMethod( new_interface1 ) ;
org_body.CallMethod( new_interface2 ) ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewBody1::Method1()
NewBody1::Method2()
NewBody2::Method1()
OrgInterface::Method2()
*/
|
次は、個人的に気に入っている「包含」を使った書き方です。 包含と継承を使って、インターフェースを実装しています。
その1〜その3で使ってきたパラメータの方式では、 ダイナミックにインターフェースの変更ができますが、
逆に、毎回インターフェースを指定しなければならない というデメリットもあります。
包含の方式では、宣言時に一度インターフェースを決めると二度と変更することは
できなくなります。 しかし、毎回指定する必要は無くなり、すっきりとしたコードになります。
(個人的には、変更できないほうが良いように思います。)
// 包含 contain + 継承 inherit
#include < stdio.h > // puts(), getchar()
// インターフェース(実装付き)
class OrgInterface
{
public:
virtual void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
virtual void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
} ;
// ボディ(OrgInterface を利用して実装されている)
class OrgBody
{
private: OrgInterface & org ; // 包含 contain
public:
OrgBody( OrgInterface & in_org ) : org( in_org ) { }
public:
void CallMethod()
{
org.Method1() ;
org.Method2() ;
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
virtual void Method1()
{
puts( "NewInterface1::Method1()" ) ;
}
// 新しいメソッドに置き換える
public:
virtual void Method2()
{
puts( "NewInterface1::Method2()" ) ;
}
} ;
class NewInterface2 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
virtual void Method1()
{
puts( "NewInterface2::Method1()" ) ;
}
// オリジナルのメソッドを使う(置き換えない)
// public: virtual void Method2()
// {
// puts( "NewInterface2::Method2()" ) ;
// }
} ;
//---------------------------
// 実装
int main( void )
{
// 新しいインターフェースの実体を作る。
NewInterface1 new_interface1 ;
NewInterface2 new_interface2 ;
// ボディの実体を作る。
OrgBody org_body1( new_interface1 ) ;
OrgBody org_body2( new_interface2 ) ;
// インターフェースはボディ宣言時に決まるので、
// 途中でインターフェースの変更はできない。
org_body1.CallMethod() ;
org_body2.CallMethod() ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewInterface1::Method1()
NewInterface1::Method2()
NewInterface2::Method1()
OrgInterface::Method2()
*/
|
包含とテンプレートを使う方法です。
その2の方法と同じように、継承ではなく、継承の変わりにテンプレート関数を使っての実装方法です。
(個人的には、継承が嫌いなので、かなり気に入っている書き方でもあります。)
デメリットとして、オリジナルのインターフェースが無いというのは、
インターフェースを実装するときに困るように思えます。
// 包含 contain + テンプレートtemplate
#include < stdio.h > // printf(), getchar()
/* インターフェースの記述は必要ない
class OrgInterface
{
public:
void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
};
*/
// ボディ( _T を利用して実装されている)
template< class _T >
class OrgBody
{
private: _T t ; // 包含 contain
public:
void CallMethod()
{
t.Method1();
t.Method2();
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1
{
// メソッドの実装(置き換えるという考え方ではない)
public:
void Method1()
{
puts( "NewInterface1::Method1()" ) ;
}
public:
void Method2()
{
puts( "NewInterface1::Method2()" ) ;
}
} ;
class NewInterface2
{
// メソッドの実装(置き換えるという考え方ではない)
public:
void Method1()
{
puts( "NewInterface2::Method1()" ) ;
}
public:
void Method2()
{
puts( "NewInterface2::Method2()" ) ;
}
} ;
//---------------------------
// 実装
int main( void )
{
// 新しいボディの実体を作る。
// ここで、すでにインターフェースは決まる。
OrgBody< NewInterface1 > org_body1 ;
OrgBody< NewInterface2 > org_body2 ;
// インターフェースはボディ宣言時に決まるので、
// 途中でインターフェースの変更はできない。
org_body1.CallMethod() ;
org_body2.CallMethod() ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewInterface1::Method1()
NewInterface1::Method2()
NewInterface2::Method1()
NewInterface2::Method2()
*/
|
で、やはり、その3と同じように、 その4とその5を合わせたような方式です。
包含、テンプレート、継承、とC++の機能をフルに使った方式とも言えます。
これも、その3と同じように、継承を使っても使わなくてもよいところが特徴と言えば特徴です。
その1〜その6に書いた書き方の中では、おそらく、この書き方が 一番汎用性が高く、記述しやすい書き方ではないかと思います。
// 包含 contain + テンプレート template + 継承 inherit
#include < stdio.h > // printf(), getchar()
// インターフェース(実装付き)
class OrgInterface
{
public:
void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
};
// ボディ( _T を利用して実装されている)
template< class _T > // テンプレート
class OrgBody
{
private: _T org ; // 包含 contain
public:
void CallMethod()
{
org.Method1() ;
org.Method2() ;
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewInterface1 : public OrgInterface // 継承 inherit
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody1::Method1()" ) ;
}
// 新しいメソッドに置き換える
public:
void Method2()
{
puts( "NewBody1::Method2()" ) ;
}
} ;
class NewInterface2 : public OrgInterface
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody2::Method1()" ) ;
}
// オリジナルのメソッドを使う(置き換えない)
// public: void Method2()
// {
// puts( "NewBody2::Method2()" ) ;
// }
};
//---------------------------
// 実装
int main( void )
{
// 新しいボディの実体を作る。
OrgBody< NewInterface1 > new_body1 ;
OrgBody< NewInterface2 > new_body2 ;
// インターフェースは NewBody クラス宣言時に決まるので、
// 途中でインターフェースの変更はできない。
new_body1.CallMethod() ;
new_body2.CallMethod() ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewBody1::Method1()
NewBody1::Method2()
NewBody2::Method1()
OrgInterface::Method2()
*/
|
テンプレート+キャスト+多重継承 最後に、参考までに、某マイクロソフトが開発(?)した
インターフェースの記述を紹介します。 なんと、自分自身をテンプレートとして使い、さらにキャスト、そして多重継承と、
なんともコンパイラ泣かせなコーディングです。 よくこんなコードが通るものだと、関心してしまいます。
よーく読まないと、なにがどうなっているのかさっぱり理解できませんが、 「インターフェースとボディを多重継承して、新しいボディを作っている」と
考えると理解しやすいでしょう。 確かに、こう書けば書けるというのはわかりますが、可読性やメンテナンスを考えると
少し引いてしまいます。 「無条件に信じる」のは止めましょう。 もう少し客観的に見て、本当にこの書き方にメリットがあるのかどうか、
各自で確かめることをお勧めします。
// テンプレート template +キャスト + 多重継承
#include < stdio.h > // printf(), getchar()
// インターフェース(実装付き)
class OrgInterface
{
public:
void Method1()
{
puts( "OrgInterface::Method1()" ) ;
}
public:
void Method2()
{
puts( "OrgInterface::Method2()" ) ;
}
} ;
// ボディ( _T を利用して実装されている)
template< class _T > // テンプレート
class OrgBody
{
public:
void CallMethod()
{
(static_cast< _T * >(this))->Method1() ; // キャスト
(static_cast< _T * >(this))->Method2() ; // キャスト
}
} ;
//---------------------------
// 新しいインターフェースを作る
class NewBody1 : public OrgBody< NewBody1 > // 変則テンプレート
, public OrgInterface // なんと多重継承
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody1::Method1()" ) ;
}
// 新しいメソッドに置き換える
public:
void Method2()
{
puts( "NewBody1::Method2()" ) ;
}
} ;
class NewBody2 : public OrgBody< NewBody2 > // 変則テンプレート
, public OrgInterface // なんと多重継承
{
// 新しいメソッドに置き換える
public:
void Method1()
{
puts( "NewBody2::Method1()" ) ;
}
// オリジナルのメソッドを使う(置き換えない)
// public: void Method2()
// {
// puts( "NewBody2::Method2()" ) ;
// }
} ;
//---------------------------
// 実装
int main( void )
{
// 新しいボディの実体を作る。
NewBody1 new_body1 ;
NewBody2 new_body2 ;
// インターフェースは NewBody クラス宣言時に決まるので、
// 途中でインターフェースの変更はできない。
new_body1.CallMethod() ;
new_body2.CallMethod() ;
getchar() ; // 終了前に一時停止
return 0 ;
}
/* 実行結果
NewBody1::Method1()
NewBody1::Method2()
NewBody2::Method1()
OrgInterface::Method2()
*/
|
このようにC++では、いろいろなインターフェースの書き方が存在します。
どの書き方が良いか?という答えは簡単には出ないでしょう。 おそらく「この場合はこの書き方」などと、
その場にあわせた書き方になるような気もします。 個人的には「その6」のような書き方が一番気に入っていますが、
まだまだ、考えが変わる可能性はあります。
以下に、その1〜その6までの各書き方のメリット/デメリットをまとめてみました。
| 書き方 |
1 2 3 4 5 6 |
メリット |
デメリット |
| パラメータ |
○ ○ ○ − − − |
ダイナミックに変更可能 |
毎回指定する必要がある |
| 包含 |
− − − ○ ○ ○ |
コードがすっきりとして可読性が良い |
途中で変更できない |
| 継承 |
○ − ○ ○ − ○ |
インターフェースの仕様が明確になる。オリジナルインターフェースが利用可能。 |
インターフェースを必ず書く必要がある |
| テンプレート |
− ○ ○ − ○ ○ |
インターフェースの記述が無くても良い。継承を使わずに実装できるので、オブジェ
クトを軽く作ることができる。 |
インターフェースの仕様が不明確になる。 |
パラメータと包含の書き方は、排他的に見えます。 両方を混在する書き方もできるとは思いますが、使う立場から考えると
どちらかに統一されていたほうが理解しやすいと思われます。 個人的には、包含の書き方が好みです。
継承とテンプレートの書き方は排他的ではなく、混在する書き方も可能です。
混在する書き方で、両者のデメリットがある程度緩和できるように思えます。
個人的に、継承は好きではないのですが、インターフェース継承まで嫌うわけには
いかないようです。
この考察の結果、やはり「その6」の書き方に一番メリットがあるように見えます。 場合によっては他の書き方が有効な時もあるでしょうが、それはその都度臨機応変に 対応するということにして、インターフェースの書き方の基本は「その6」で 一旦、決着をつけることにします。
もし、この文章を読んだ人のなかで、他に
「こんな書き方のほうが良いのでは?」という案などありましたら、
掲示板のほうへドシドシ書き込んでください。お待ちしています。
現状では、ここまでです。 コードの紹介で終わりです。 そのうち、解説が書けるといいなぁとは思っています。
が、いつになることやら。
last update 2002/03/05
since 2000/05/23
|