Atsushi4のブログ

へたれQtプログラマの備忘録的ななにか

インターフェースクラスと抽象クラスについて

はじめに

これはETロボコン Advent Calendar 2014 - Adventarの15日目のエントリーです。13,14日目はOkazakiYuhei (@Y_uuu) | Twitterさんによるモックのお話でした。
今日はインターフェースクラスと抽象クラスについてのお話です。東海地区大会2日目のモデリングワークショップにおいて、本部審査委員長の渡辺博之さんがとても分かりやすく説明してくださったので共有したいと思って書いています。
ご指摘、質問歓迎します。


抽象クラス、インターフェースクラスとは

さっそく説明を見ていきます。

渡辺さんの説明

例えば動物を抽象化した抽象クラス「動物」と車を抽象化した「車」クラスがあるとして、どちらも「走る」という振舞いを持っているとすると、この「走る」という振舞いをインターフェースクラスとして抜き出せるかもしれないね、というお話でした。クラス図を書くと下のようになります。

fig.1 インターフェースクラス使用前
f:id:Atsushi4:20141215082930p:plain

fig.2 インターフェースクラス使用後
f:id:Atsushi4:20141215083607p:plain

渡辺さんの説明はここまでです。

抽象クラスとは

抽象クラスとは、上の「動物」クラスや「車」クラスのように、共通の性質を持つクラス群を抽象化したクラスです。抽象クラスの特徴は次のとおりです。

インターフェースクラスとは

インターフェースクラスとは、上の「走るインターフェース」クラスのように、通常異なる性質を持つ複数のクラス間において、共通する振舞いのインターフェースを規定するクラスです。インターフェースクラスの特徴は次のとおりです。

  • インターフェースクラスのインスタンスを生成することはできない。
  • インターフェースクラスは実装を持たない。

C++の実装から見た抽象クラスとインターフェースクラス

C++ではそれぞれのクラスがどのように実装されるのか見ていきます。

抽象クラスの実装例

抽象クラスの実装例は次のとおりです。

class Animal
{
protected:
    Animal(){}
public:
    virtual ~Animal(){}
public:
    virtual void run();
};

C++の実装における抽象クラスの特徴は次のとおりです。

  • 抽象クラスのコンストラクタはprotectedにする。
  • 抽象クラスのデストラクタは、抽象クラスのポインタを介して破棄することを許容しない場合はprotected非仮想に、それ以外はpublic virtualにする。
  • 抽象クラスはメンバー変数、メンバー関数(非仮想、仮想、純粋仮想)のいずれも持つことができる。

インターフェースクラスの実装例

インターフェースクラスの実装例は次のとおりです。

class IRunnable
{
public:
    virtual ~IRunnable() {}
public:
    virtual void run() = 0;
};

C++の(望ましい)実装におけるインターフェースクラスの特徴は次のとおりです。

  • publicな純粋仮想関数(なるべく1つ)のみを持つ。
  • インターフェースクラスのデストラクタは、インターフェースクラスのポインタを介して破棄することを許容しない場合はprotected非仮想に、それ以外はpublic virtualにする。

*1デストラクタの実装は上の方に書いた「インターフェースクラスは実装を持たない」に反しますが、C++では派生クラスのメンバーをリークさせないために必要ですね。

インターフェースクラスの利点

例えば動物クラスと車クラスの例では、全てのインスタンスを走らせるために2つのリストをtemplateを使って走査しなければならないかもしれません。

template<typename T>
void doRun(T object) {
    object.run();
}

Animal animals[10];
Car       cars[5];

void runAll() {
    std::for_each(animals, animals + 10, doRun<Animal>);
    std::for_each(cars, cars + 5, doRun<Car>);
}

インターフェースクラスを使うと、1つのリストで扱えます。

void doRun(IRunnable *runnable) {
    runnable->run();
}

Animal animals[10];
Car       cars[5];
IRunnable *objects[15];

void runAll() {
    std::for_each(objects, objects + 15, doRun);
}

インターフェースクラスを使うと振舞いのインターフェースを厳密に決めることができ、戻り値の違いやデフォルト引数などによる関数のシグネチャの違いが無い事を保障することができます。templateを使った例では、runを関数ポインタのpublicメンバー変数にするようなトリッキーなこともできちゃいます。(そっちの方が柔軟だ、という人もいるかもしれませんが)


おわりに

抽象クラスとインターフェースクラスについて書きました。ETロボコンモデリングにおいて、「動物」や「車」(あるいは「銀行」:P)のような、モノを抽象化したクラスに<interface>と書いたら、「あれ、設計がおかしいかもしれない」と見直してみると良いかもしれません。
ちなみにうちのチームのモデルシートは<interface>の人が集約の全体クラスになったりしていてあばばばばば。

2回続けて記事の公開が翌日になってしまいましたごめんなさい。

*1:12/17 デストラクタの記述を追記