Menu
Coddy logo textTech

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; }
};

インターフェースは疎結合を促進します。コードが具体的な実装ではなく抽象に依存するようになるため、拡張やテストが容易になります。

challenge icon

チャレンジ

簡単

インターフェースが異なるタイプのデバイスに対してどのようにコントラクト(規約)を定義するかを示す、デバイス管理システムを構築しましょう。デバイスが実装できる2つの別々のインターフェースを作成し、それからこれらのコントラクトの片方または両方を満たす具体的なデバイスクラスを構築します。

コードは以下の3つのファイルに分けて構成します:

  • Interfaces.h: 異なるデバイスの能力を表す2つの純粋仮想クラス(インターフェース)を定義します:

    Powerable — 電源のオン・オフが可能なすべてのデバイス:

    • 純粋仮想関数 powerOn() メソッド
    • 純粋仮想関数 powerOff() メソッド
    • std::string を返す純粋仮想関数 getPowerStatus() メソッド
    • 仮想デストラクタ

    Connectable — ネットワークに接続可能なすべてのデバイス:

    • 純粋仮想関数 connect(const std::string& network) メソッド
    • 純粋仮想関数 disconnect() メソッド
    • std::string を返す純粋仮想関数 getConnectionInfo() メソッド
    • 仮想デストラクタ
  • Devices.h: これらのインターフェースを使用する具体的なデバイスクラスを実装します:

    LampPowerable のみを実装します:

    • プライベートな bool isOn メンバ(初期値は false
    • プライベートな std::string name メンバ
    • ランプの名前を受け取るコンストラクタ
    • powerOn()isOn を true に設定し、<name>: Light turned on と出力します
    • powerOff()isOn を false に設定し、<name>: Light turned off と出力します
    • getPowerStatus() は状態に基づいて "ON" または "OFF" を返します

    SmartTVPowerableConnectable の両方を実装します:

    • プライベートメンバ:bool isOn(初期値は false)、std::string brandstd::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つの入力(それぞれ別々の行)を読み込みます:
    1. ランプの名前
    2. TVのブランド名
    3. ネットワーク名

    LampSmartTV を作成します。同じインターフェースを異なるデバイスでどのように使用できるかを示します:

    まず、Powerable インターフェースを通じて両方のデバイスを操作します。両方のポインタを Powerable* の配列に格納し、ループを回してそれぞれに対して powerOn() を呼び出し、続いてそのステータスを Status: <powerStatus> として出力します。

    空行を1行出力し、次に Connectable インターフェースを通じて SmartTV を操作します。SmartTV への Connectable* ポインタを作成し、ネットワーク名で connect() を呼び出し、Connection: <connectionInfo> と出力します。

    さらにもう1行空行を出力し、Powerable 配列を通じて両方のデバイスの電源をオフにし、最終的なステータスを表示します。

例えば、入力が Desk LampSamsungHomeWiFi の場合:

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

使用するインターフェースポインタに応じて、SmartTVPowerable としても 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;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン