C++におけるインターフェース設計
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 61/104。
C++におけるインターフェースとは、すべてのメンバ関数が純粋仮想関数である抽象クラスのことです。一部の実装を含むことがある抽象クラスとは異なり、インターフェースはどのような操作が存在しなければならないかのみを定義します。つまり、振る舞いを伴わずに契約を指定するものです。
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};この Drawable インターフェースは、実装するすべてのクラスが draw() メソッドと resize() メソッドを持つことを保証しますが、それらがどのように動作するかについては何も言及しません。この分離は強力です。コードは具体的な型について何も知らなくても、インターフェースに依存することができます。
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};
class Button : public Drawable {
std::string label;
public:
Button(std::string l) : label(l) {}
void draw() override { std::cout << "Drawing button: " << label << std::endl; }
void resize(double factor) override { /* ボタンをリサイズ */ }
};クラスは複数のインターフェースを実装することができ、オブジェクトが異なる役割を果たすことができる柔軟な設計が可能になります:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};インターフェースは疎結合を促進します。コードが具体的な実装ではなく抽象に依存するようになるため、拡張やテストが容易になります。
チャレンジ
簡単インターフェースが異なるタイプのデバイスに対してどのようにコントラクト(規約)を定義するかを示す、デバイス管理システムを構築しましょう。デバイスが実装できる2つの別々のインターフェースを作成し、それからこれらのコントラクトの片方または両方を満たす具体的なデバイスクラスを構築します。
コードは以下の3つのファイルに分けて構成します:
Interfaces.h: 異なるデバイスの能力を表す2つの純粋仮想クラス(インターフェース)を定義します:Powerable— 電源のオン・オフが可能なすべてのデバイス:- 純粋仮想関数
powerOn()メソッド - 純粋仮想関数
powerOff()メソッド std::stringを返す純粋仮想関数getPowerStatus()メソッド- 仮想デストラクタ
Connectable— ネットワークに接続可能なすべてのデバイス:- 純粋仮想関数
connect(const std::string& network)メソッド - 純粋仮想関数
disconnect()メソッド std::stringを返す純粋仮想関数getConnectionInfo()メソッド- 仮想デストラクタ
- 純粋仮想関数
Devices.h: これらのインターフェースを使用する具体的なデバイスクラスを実装します:Lamp—Powerableのみを実装します:- プライベートな
bool isOnメンバ(初期値はfalse) - プライベートな
std::string nameメンバ - ランプの名前を受け取るコンストラクタ
powerOn()はisOnを true に設定し、<name>: Light turned onと出力しますpowerOff()はisOnを false に設定し、<name>: Light turned offと出力しますgetPowerStatus()は状態に基づいて"ON"または"OFF"を返します
SmartTV—PowerableとConnectableの両方を実装します:- プライベートメンバ:
bool isOn(初期値は false)、std::string brand、std::string currentNetwork(初期値は空) - TVのブランド名を受け取るコンストラクタ
powerOn()はisOnを true に設定し、<brand> TV: Powered onと出力しますpowerOff()はisOnを false に設定し、ネットワークをクリアして、<brand> TV: Powered offと出力しますgetPowerStatus()は"ON"または"OFF"を返しますconnect()はネットワーク名を保存し、<brand> TV: Connected to <network>と出力しますdisconnect()はネットワークをクリアし、<brand> TV: Disconnectedと出力しますgetConnectionInfo()は、接続されている場合は"Connected to <network>"を、空の場合は"Not connected"を返します
- プライベートな
main.cpp: 3つの入力(それぞれ別々の行)を読み込みます:- ランプの名前
- TVのブランド名
- ネットワーク名
LampとSmartTVを作成します。同じインターフェースを異なるデバイスでどのように使用できるかを示します:まず、
Powerableインターフェースを通じて両方のデバイスを操作します。両方のポインタをPowerable*の配列に格納し、ループを回してそれぞれに対してpowerOn()を呼び出し、続いてそのステータスをStatus: <powerStatus>として出力します。空行を1行出力し、次に
Connectableインターフェースを通じて SmartTV を操作します。SmartTV へのConnectable*ポインタを作成し、ネットワーク名でconnect()を呼び出し、Connection: <connectionInfo>と出力します。さらにもう1行空行を出力し、
Powerable配列を通じて両方のデバイスの電源をオフにし、最終的なステータスを表示します。
例えば、入力が Desk Lamp、Samsung、HomeWiFi の場合:
Desk Lamp: Light turned on
Status: ON
Samsung TV: Powered on
Status: ON
Samsung TV: Connected to HomeWiFi
Connection: Connected to HomeWiFi
Desk Lamp: Light turned off
Status: OFF
Samsung TV: Powered off
Status: OFF使用するインターフェースポインタに応じて、SmartTV が Powerable としても Connectable としても扱えることに注目してください。この柔軟性が複数のインターフェースを実装することの利点です。コードは、具体的な型を知らなくても、必要なコントラクトを満たすすべてのデバイスと連携できます。
チートシート
C++におけるインターフェースは、すべてのメンバ関数が純粋仮想関数である抽象クラスです。インターフェースは、実装を持たずに、どのような操作が存在しなければならないかのみを定義します:
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};クラスは、すべての純粋仮想関数をオーバーライドすることでインターフェースを実装します:
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};クラスは、多重継承を使用して複数のインターフェースを実装できます:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};インターフェースは、コードが具体的な実装ではなく抽象に依存することを可能にすることで、疎結合を促進します。
自分で試してみよう
#include <iostream>
#include <string>
#include "Devices.h"
using namespace std;
int main() {
// 入力を読み込む
string lampName;
string tvBrand;
string networkName;
getline(cin, lampName);
getline(cin, tvBrand);
getline(cin, networkName);
// TODO: Lamp と SmartTV のオブジェクトを作成する
// TODO: 両方のデバイスを含む Powerable* ポインタの配列を作成する
// TODO: 配列をループし、各デバイスで powerOn() を呼び出す
// 各 powerOn() の後、次のように出力する: Status: <powerStatus>
// TODO: 空行を出力する
// TODO: SmartTV への Connectable* ポインタを作成する
// ネットワーク名を指定して connect() を呼び出す
// 次のように出力する: Connection: <connectionInfo>
// TODO: 空行を出力する
// TODO: Powerable 配列をループし、各デバイスで powerOff() を呼び出す
// 各 powerOff() の後、次のように出力する: Status: <powerStatus>
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
8ポリモーフィズム
コンパイル時 vs 実行時ポリモーフィズム関数オーバーロード仮想関数の再考純粋仮想関数抽象クラスC++におけるインターフェース設計Dynamic Casting & RTTIまとめ:図形計算機3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス