カプセル化と保護属性

これまでに定義した構造体およびクラスはすべて、外部からアクセス可能だった。

次の構造体を考えてみよう。

enum Gender { female, male }

struct Student {
    string name;
    Gender gender;
}
D

この構造体のメンバーは、プログラム内の他の部分から自由にアクセスできる。

auto student = Student("Tim", Gender.male);
writefln("%s is a %s student.", student.name, student.gender);
D

このような自由は、プログラムでは便利だ。例えば、前の行は、次の出力を生成するのに役立った。

ティムは男子学生です。

しかし、この自由は欠点でもある。例えば、誤ってプログラム内で学生オブジェクトの名前が変更された場合を考えてみよう。

student.name = "Anna";
D

この代入はオブジェクトを無効な状態にする可能性がある:

アンナ男子学生です。

別の例として、Schoolクラスを考えてみよう。このクラスには、男子学生と女子学生の数を別々に格納する2つのメンバー変数があると仮定しよう。

class School {
    Student[] students;
    size_t femaleCount;
    size_t maleCount;

    void add(Student student) {
        students ~= student;

        final switch (student.gender) {

        case Gender.female:
            ++femaleCount;
            break;

        case Gender.male:
            ++maleCount;
            break;
        }
    }

    override string toString() const {
        return format("%s female, %s male; total %s students",
                      femaleCount, maleCount, students.length);
    }
}
D

add()メンバー関数は、カウントが常に正しいことを確認しながら、学生を追加する。

auto school = new School;
school.add(Student("Lindsey", Gender.female));
school.add(Student("Mark", Gender.male));
writeln(school);
D

プログラムは次の一貫した出力を生成する:

女子1名、男子1名; 合計2名の学生

ただし、Schoolのメンバに自由にアクセスできる場合、この一貫性が常に維持される保証はない。studentsメンバに新しい要素を直接追加する場合を考えてみよう:

school.students ~= Student("Nancy", Gender.female);
D

新しい学生は、add()メンバー関数を経由せずに直接配列に追加されたため、Schoolオブジェクトは不整合な状態になっている。

女子1名、男子1名; 合計3名の学生
カプセル化

カプセル化は、上記の例のような問題を回避するために、メンバーへのアクセスを制限するプログラミングの概念だ。

カプセル化のもう1つの利点は、型の実装の詳細を知る必要がなくなることだ。ある意味で、カプセル化により、型はインターフェースを通じてのみ使用されるブラックボックスとして表現できる。

さらに、ユーザーがメンバーに直接アクセスできないようにすることで、将来、クラスのメンバーを自由に変更できるようになる。クラスのインターフェースを定義する関数が同じままであれば、その実装は自由に変更できる。

カプセル化は、クレジットカード番号やパスワードなどの機密データへのアクセスを制限するためのものではなく、その目的には使用できない。カプセル化は開発ツールであり、型を簡単かつ安全に使用およびコーディングできるようにする。

保護属性

保護属性は、構造体、クラス、およびモジュールのメンバーへのアクセスを制限する。保護属性を指定するには2つの方法がある。

保護属性は、次のキーワードで指定できる。デフォルト属性はpublicだ。

さらに、export属性は、プログラムの外側からのアクセス可能性を指定する。

定義

保護属性は3つの方法で指定できる。

単一の定義の前に記述すると、その定義のみの保護属性を指定する。これはJavaプログラミング言語に類似している:

private int foo;

private void bar() {
    // ...
}
D

コロンで指定すると、次の保護属性の指定まで、その後に続くすべての定義の保護属性を指定する。これは C++ プログラミング言語と類似している:

private:
    // ...
    // ... ここにある定義はすべてprivate ...
    // ...

protected:
    // ...
    // ... ここにある定義はすべてprotected ...
    // ...
D

ブロックに対して指定された場合、そのブロック内のすべての定義に保護属性が適用される:

private {
    // ...
    // ... ここにある定義はすべてprivate ...
    // ...
}
D
モジュールインポートはデフォルトでプライベート

importによってインポートされたモジュールは、それをインポートしたモジュールに対してプライベートになる。間接的にそれをインポートした他のモジュールからは参照できない。例えば、schoolモジュールがstd.stdioをインポートしている場合、schoolをインポートしたモジュールは、std.stdioモジュールを自動的に使用することはできない。

schoolモジュールが次の行で開始すると仮定しよう。

module school.school;

import std.stdio;    // このモジュールで独自に使用するためにインポート...

// ...
D

次のプログラムは、writelnが参照できないためコンパイルできない:

import school.school;

void main() {
    writeln("hello");    // ← コンパイルエラー
}
D

std.stdio そのモジュールもインポートする必要がある。

import school.school;
import std.stdio;

void main() {
    writeln("hello");   // 現在コンパイルできる
}
D

モジュールが他のモジュールを間接的に表現することが望ましい場合がある。例えば、schoolモジュールが、そのユーザーのためにstudentモジュールを自動的にインポートすることは理にかなっている。これは、importpublicを指定することで実現できる。

module school.school;

public import school.student;

// ...
D

この定義により、schoolをインポートするモジュールは、studentモジュールをインポートしなくても、その中の定義を使用することができる。

import school.school;

void main() {
    auto student = Student("Tim", Gender.male);

    // ...
}
D

上記のプログラムはschoolモジュールのみをインポートしているが、student.Student構造体も使用可能である。

カプセル化を使用するタイミング

カプセル化により、この章の冒頭で見たような問題を回避できる。カプセル化は、オブジェクトが常に一貫した状態であることを保証するための非常に貴重なツールだ。カプセル化は、型のユーザーによるメンバーの直接的な変更からメンバーを保護することで、構造体およびクラスの不変条件を維持するのに役立つ。

カプセル化により、実装をユーザーコードから抽象化することで、実装の自由度が高まる。例えば、ユーザーがSchool.studentsに直接アクセスできる場合、その配列を連想配列に変更するなどしてクラスの設計を変更することは困難である。これは、そのメンバーにアクセスしているすべてのユーザーコードに影響を与えるためだ。

カプセル化は、オブジェクト指向プログラミングの最も強力な利点の1つである。

カプセル化を利用して、Student構造体とSchoolクラスを定義し、短いテストプログラムで使用しよう。

このサンプルプログラムは3つのファイルで構成される。前の章で覚えているように、schoolパッケージの一部である2つのファイルは "school" ディレクトリにある。

以下は"school/student.d"ファイルの内容だ:

module school.student;

import std.string;
import std.conv;

enum Gender { female, male }

struct Student {
    package string name;
    package Gender gender;

    string toString() const {
        return format("%s is a %s student.",
                      name, to!string(gender));
    }
}
D

この構造体のメンバーは、同じパッケージのモジュールからのみアクセスできるように、packageとマークされている。まもなく、Schoolがこれらのメンバーに直接アクセスすることがわかるだろう。(これはカプセル化の原則に反すると考えるべきであることに注意。ただし、このサンプルプログラムでは、package属性を使用することにする。)

以下の"school/school.d"モジュールは、前のモジュールを使用している。

module school.school;

public import school.student;                  // 1

import std.string;

class School {
private:                                       // 2

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

public:                                        // 3

    void add(Student student) {
        students ~= student;

        final switch (student.gender) {        // 4a

        case Gender.female:
            ++femaleCount;
            break;

        case Gender.male:
            ++maleCount;
            break;
        }
    }

    override string toString() const {
        string result = format(
            "%s female, %s male; total %s students",
            femaleCount, maleCount, students.length);

        foreach (i, student; students) {
            result ~= (i == 0) ? ": " : ", ";
            result ~= student.name;            // 4b
        }

        return result;
    }
}
D
  1. school.studentは、school.schoolのユーザーが明示的にそのモジュールをインポートする必要がないように、パブリックにインポートされている。ある意味で、studentモジュールはschoolモジュールによって利用可能になっている。
  2. Schoolのすべてのメンバー変数はprivateとしてマークされている。これは、このクラスのメンバー変数の整合性を保護するために重要だ。
  3. このクラスが有用であるためには、いくつかのメンバー関数が必要である。add()およびtoString()は、このクラスのユーザーが利用できる。
  4. Studentの2つのメンバー変数はpackageとしてマークされているため、同じパッケージの一部であるSchoolはこれらの変数にアクセスできる。

最後に、これらの型を使用するテストプログラムを示す。

import std.stdio;
import school.school;

void main() {
    auto student = Student("Tim", Gender.male);
    writeln(student);

    auto school = new School;

    school.add(Student("Lindsey", Gender.female));
    school.add(Student("Mark", Gender.male));
    school.add(Student("Nancy", Gender.female));

    writeln(school);
}
D

このプログラムは、StudentおよびSchoolを、そのパブリックインターフェースを通じてのみ使用できる。これらの型のメンバー変数にはアクセスできない。その結果、オブジェクトは常に一貫性がある。

ティムは男子学生です。
女子2人、男子1人、合計3人の学生: リンジー、マーク、ナンシー

このプログラムは、Schooladd()およびtoString()関数によってのみSchoolと対話することに注意。これらの関数のインターフェースが同じである限り、その実装を変更しても、上記のプログラムには影響はない。