インターフェース

interfaceキーワードは、クラス階層内でインターフェースを定義するために使用される。interfaceは、以下の制限を除いてclassと非常に似ている:

これらの制限にもかかわらず、クラスが継承できるinterfaceの数に制限はない。(対照的に、クラスは最大1つのclassを継承できる。)

定義

インターフェースは、クラスと同じようにinterfaceキーワードで定義される:

interface SoundEmitter {
    // ...
}
D

interfaceは、暗黙的に抽象であるメンバー関数を宣言するためのものだ。

interface SoundEmitter {
    string emitSound();    // 宣言済み(実装されていない)
}
D

そのインターフェースを継承するクラスは、インターフェースの抽象関数の実装を提供する必要がある。

インターフェース関数の宣言には、inおよびoutの契約ブロックを含めることができる。

interface I {
    int func(int i)
    in {
        /* この関数の呼び出し元が満たさなければならない最も厳しい要件。
         * (派生インターフェースおよびクラスは、
         * これらの要件を緩和することができる。) */

    } out {    // (オプションで(結果)パラメータ付き)
        /* この関数の実装が提供しなければならない終了保証。
         * (派生インターフェースおよびクラスは、
         * 追加の保証を提供することができる。) */
    }
}
D

契約継承の例は、後で構造体およびクラスの契約プログラミングの章で説明する。

インターフェースから継承するinterface

interfaceの継承構文は、classの継承構文と同じである:

class Violin : SoundEmitter {
    string emitSound() {
        return "♩♪♪";
    }
}

class Bell : SoundEmitter {
    string emitSound() {
        return "ding";
    }
}
D

インターフェースは多態性をサポートしている。インターフェースパラメータを取る関数は、オブジェクトの実際の型を知らなくても、そのパラメータを使用することができる。例えば、SoundEmitterパラメータを取る次の関数は、オブジェクトの実際の型を知らなくても、そのパラメータに対してemitSound()を呼び出す。

void useSoundEmittingObject(SoundEmitter object) {
    // ... いくつかの操作 ...
    writeln(object.emitSound());
    // ... さらに操作 ...
}
D

クラスと同様に、この関数は、SoundEmitterインターフェースを継承するあらゆる型のオブジェクトで呼び出すことができる。

useSoundEmittingObject(new Violin);
useSoundEmittingObject(new Bell);
D

各オブジェクトの特別なemitSound()関数が呼び出され、Violin.emitSoundおよびBell.emitSoundの出力が表示される。

♩♪♪
ding
複数のinterfaceから継承する

クラスは、最大1つのclassから継承できる。継承できるinterfaceの数に制限はない。

通信デバイスを表す次のインターフェースを考えてみよう:

interface CommunicationDevice {
    void talk(string message);
    string listen();
}
D

Phoneクラスが、音の発生装置としても通信デバイスとしても使用される必要がある場合、そのクラスは両方のインターフェースを継承することができる:

class Phone : SoundEmitter, CommunicationDevice {
    // ...
}
D

この定義は、"電話は音の発生装置である"と"電話は通信デバイスである"という両方の関係を表現している。

このクラスのオブジェクトを構築するには、Phoneは両方のインターフェースの抽象関数を実装する必要がある。

class Phone : SoundEmitter, CommunicationDevice {
    string emitSound() {           // SoundEmitter用
        return "rrring";
    }

    void talk(string message) {    // CommunicationDevice用
        // ... 行にメッセージを入力 ...
    }

    string listen() {              // CommunicationDevice用
        string soundOnTheLine;
        // ... 行からメッセージを取得 ...
        return soundOnTheLine;
    }
}
D

クラスは、プログラムの設計に応じて、任意の数のインターフェースを継承できる。

interfaceから継承し、class

クラスは、最大1つのclassから継承することもできる:

class Clock {
    // ... clockの実装 ...
}

class AlarmClock : Clock, SoundEmitter {
    string emitSound() {
        return "beep";
    }
}
D

AlarmClockは、Clockのメンバーを継承する。さらに、SoundEmitterインターフェースが要求するemitSound()関数も提供する。

interfaceを継承するinterface

別のインターフェースから継承されたインターフェースは、サブクラスが実装しなければならない関数の数を事実上増やす。

interface MusicalInstrument : SoundEmitter {
    void adjustTuning();
}
D

上記の定義によると、MusicalInstrumentになるためには、SoundEmitterが要求するemitSound()関数と、MusicalInstrumentが要求するadjustTuning()関数の両方を実装しなければならない。

例えば、ViolinSoundEmitterではなくMusicalInstrumentを継承する場合、adjustTuning()も実装する必要がある。

class Violin : MusicalInstrument {
    string emitSound() {     // SoundEmitter用
        return "♩♪♪";
    }

    void adjustTuning() {    // MusicalInstrument用
        // ... バイオリンの特別な調律 ...
    }
}
D
staticメンバー関数

staticメンバー関数の説明は、前の章を短くするため、この章まで延期した。staticメンバー関数は、構造体、クラス、およびインターフェースで使用できる。

通常のメンバー関数は、常にオブジェクトに対して呼び出される。メンバー関数内で参照されるメンバー変数は、特定のオブジェクトのメンバーだ。

struct Foo {
    int i;

    void modify(int value) {
        i = value;
    }
}

void main() {
    auto object0 = Foo();
    auto object1 = Foo();

    object0.modify(10);    // object0.iの変更
    object1.modify(10);    // object1.iの変更
}
D
interface.1

メンバーは、thisによっても参照できる:

void modify(int value) {
    this.i = value;    // 前のものと同等
}
D

staticメンバー関数はオブジェクトに対して動作しない。thisキーワードが参照するオブジェクトは存在しないため、thisstatic関数内では無効だ。このため、通常のメンバー変数はstaticメンバー関数内では使用できない。

struct Foo {
    int i;

    static void commonFunction(int value) {
        i = value;         // ← コンパイルエラー
        this.i = value;    // ← コンパイルエラー
    }
}
D

staticメンバー関数は、staticメンバー変数のみを使用できる。

構造体の章で前に見たPoint構造体を、今回はstaticメンバー関数を使って再設計しよう。次のコードでは、すべてのPointオブジェクトに、staticメンバー関数によって決定される一意のIDが割り当てられる。

import std.stdio;

struct Point {
    size_t id;    // オブジェクトID
    int line;
    int column;

    // 次のオブジェクトに使用するID
    static size_t nextId;

    this(int line, int column) {
        this.line = line;
        this.column = column;
        this.id = makeNewId();
    }

    static size_t makeNewId() {
        immutable newId = nextId;
        ++nextId;
        return newId;
    }
}

void main() {
    auto top = Point(7, 0);
    auto middle = Point(8, 0);
    auto bottom =  Point(9, 0);

    writeln(top.id);
    writeln(middle.id);
    writeln(bottom.id);
}
D
interface.2

static makeNewId()関数は、共通変数nextIdを使用できる。その結果、すべてのオブジェクトに一意のIDが割り当てられる。

0
1
2

上記の例にはstructが含まれているが、staticメンバー関数はクラスおよびインターフェースでも使用できる。

finalメンバー関数

finalメンバー関数の説明は、前の章を短くするため、この章まで延期した。finalメンバー関数は、構造体は継承をサポートしていないため、クラスとインターフェースにのみ関係する。

finalは、メンバー関数がサブクラスによって再定義できないことを指定する。ある意味で、このclassまたはinterfaceが提供する実装は、その関数の最終的な実装となる。この機能が役立つ例としては、アルゴリズムの一般的な手順をインターフェースで定義し、詳細をサブクラスに任せる場合が挙げられる。

Gameインターフェースを使って、この例を見てみよう。ゲームプレイの一般的な手順は、次のinterfaceplay()関数によって決定されている。

interface Game {
    final void play() {
        string name = gameName();
        writefln("Starting %s", name);

        introducePlayers();
        prepare();
        begin();
        end();

        writefln("Ending %s", name);
    }

    string gameName();
    void introducePlayers();
    void prepare();
    void begin();
    void end();
}
D
interface.3

サブクラスは、play()メンバー関数の定義を変更することはできない。サブクラスは、インターフェースで宣言されている5つの抽象メンバー関数の定義を提供することができる(そして提供しなければならない)。そうすることで、サブクラスはアルゴリズムの欠落しているステップを完了する。

import std.stdio;
import std.string;
import std.random;
import std.conv;

class DiceSummingGame : Game {
    string player;
    size_t count;
    size_t sum;

    string gameName() {
        return "Dice Summing Game";
    }

    void introducePlayers() {
        write("What is your name? ");
        player = strip(readln());
    }

    void prepare() {
        write("How many times to throw the dice? ");
        readf(" %s", &count);
        sum = 0;
    }

    void begin() {
        foreach (i; 0 .. count) {
            immutable dice = uniform(1, 7);
            writefln("%s: %s", i, dice);
            sum += dice;
        }
    }

    void end() {
        writefln("Player: %s, Dice sum: %s, Average: %s",
                 player, sum, to!double(sum) / count);
    }
}

void useGame(Game game) {
    game.play();
}

void main() {
    useGame(new DiceSummingGame());
}
D
interface.3

上記の例にはinterfaceが含まれているが、finalメンバー関数はクラスでも使用できる。

使用方法

interfaceはよく使用される機能だ。ほぼすべてのクラス階層の頂点には、1つ以上のinterfaceがある。プログラムでよく見られる階層構造の1つは、単一のinterfaceと、そのインターフェースを実装する複数のクラスからなるものである:

               MusicalInstrument
                 (インターフェイス)
               /    |     \     \
          Violin  Guitar  Flute  ...

実践ではより複雑な階層構造が存在するが、上記のシンプルな階層構造は多くの問題を解決する。

クラス階層の共通の実装詳細を中間クラスに移動することも一般的である。サブクラスはこれらの中間クラスから継承する。以下のStringInstrumentWindInstrumentクラスには、それぞれのサブクラスの共通メンバーを含めることができる:

               MusicalInstrument
                 (インターフェイス)
                 /         \
   StringInstrument       WindInstrument
     /    |     \         /      |     \
Violin  Viola    ...   Flute  Clarinet  ...

サブクラスは、それぞれのメンバー関数の特別な定義を実装する。

抽象化

インターフェースは、プログラムの各部分を互いに独立させるのに役立つ。これを抽象化という。例えば、楽器を扱うプログラムは、楽器の実際の型を指定することなく、主にMusicalInstrumentインターフェースを使用して記述することができる。

Musicianクラスは、楽器の実際の型を知らなくても、MusicalInstrumentを含むことができる。

class Musician {
    MusicalInstrument instrument;
    // ...
}
D

実際の楽器の型に関係なく、さまざまな型の楽器をコレクションに組み合わせることができる。

MusicalInstrument[] orchestraInstruments;
D

プログラムのほとんどの関数は、このインターフェースだけを使用して記述することができる。

bool needsTuning(MusicalInstrument instrument) {
    bool result;
    // ...
    return result;
}

void playInTune(MusicalInstrument instrument) {
    if (needsTuning(instrument)) {
        instrument.adjustTuning();
    }

    writeln(instrument.emitSound());
}
D

プログラムの部品を互いに抽象化することで、プログラムの1つの部品を変更しても、他の部品を変更する必要がなくなる。プログラムの特定の部品の実装が特定のインターフェースの背後にある場合、そのインターフェースのみを使用するコードは影響を受けない。

次のプログラムは、SoundEmitterMusicalInstrument、およびCommunicationDeviceインターフェースを定義している:

import std.stdio;

/* このインターフェースはemitSound()を必要とする。 */
interface SoundEmitter {
    string emitSound();
}

/* このクラスはemitSound()だけを実装すれば良い。 */
class Bell : SoundEmitter {
    string emitSound() {
        return "ding";
    }
}

/* このインターフェースはさらにadjustTuning()を必要とする。 */
interface MusicalInstrument : SoundEmitter {
    void adjustTuning();
}

/* このクラスはemitSound()とadjustTuning()の両方を
 * 実装する必要がある。 */
class Violin : MusicalInstrument {
    string emitSound() {
        return "♩♪♪";
    }

    void adjustTuning() {
        // ... バイオリンの調律 ...
    }
}

/* このインターフェースはtalk()とlisten()を必要とする。 */
interface CommunicationDevice {
    void talk(string message);
    string listen();
}

/* このクラスはemitSound()、talk()、およびlisten()を
 * 実装する必要がある。 */
class Phone : SoundEmitter, CommunicationDevice {
    string emitSound() {
        return "rrring";
    }

    void talk(string message) {
        // ... メッセージを行に追加 ...
    }

    string listen() {
        string soundOnTheLine;
        // ... 行からメッセージを取得 ...
        return soundOnTheLine;
    }
}

class Clock {
    // ... Clockの実装 ...
}

/* このクラスはemitSound()のみを実装する必要がある。 */
class AlarmClock : Clock, SoundEmitter {
    string emitSound() {
        return "beep";
    }

    // ... AlarmClockの実装 ...
}

void main() {
    SoundEmitter[] devices;

    devices ~= new Bell;
    devices ~= new Violin;
    devices ~= new Phone;
    devices ~= new AlarmClock;

    foreach (device; devices) {
        writeln(device.emitSound());
    }
}
D
interface.4

devicesSoundEmitterスライスであるため、SoundEmitterを継承するあらゆる型のオブジェクト (つまり、SoundEmitterと"は"の関係にある型) を含むことができる。その結果、プログラムの出力は、さまざまな型のオブジェクトによって出力されるさまざまな音で構成される。

ding
♩♪♪
rrring
beep
要約