カプセル化と保護属性
これまでに定義した構造体およびクラスはすべて、外部からアクセス可能だった。
次の構造体を考えてみよう。
この構造体のメンバーは、プログラム内の他の部分から自由にアクセスできる。
このような自由は、プログラムでは便利だ。例えば、前の行は、次の出力を生成するのに役立った。
ティムは男子学生です。
しかし、この自由は欠点でもある。例えば、誤ってプログラム内で学生オブジェクトの名前が変更された場合を考えてみよう。
この代入はオブジェクトを無効な状態にする可能性がある:
アンナは男子学生です。
別の例として、School
クラスを考えてみよう。このクラスには、男子学生と女子学生の数を別々に格納する2つのメンバー変数があると仮定しよう。
add()
メンバー関数は、カウントが常に正しいことを確認しながら、学生を追加する。
プログラムは次の一貫した出力を生成する:
女子1名、男子1名; 合計2名の学生
ただし、School
のメンバに自由にアクセスできる場合、この一貫性が常に維持される保証はない。students
メンバに新しい要素を直接追加する場合を考えてみよう:
新しい学生は、add()
メンバー関数を経由せずに直接配列に追加されたため、School
オブジェクトは不整合な状態になっている。
女子1名、男子1名; 合計3名の学生
カプセル化
カプセル化は、上記の例のような問題を回避するために、メンバーへのアクセスを制限するプログラミングの概念だ。
カプセル化のもう1つの利点は、型の実装の詳細を知る必要がなくなることだ。ある意味で、カプセル化により、型はインターフェースを通じてのみ使用されるブラックボックスとして表現できる。
さらに、ユーザーがメンバーに直接アクセスできないようにすることで、将来、クラスのメンバーを自由に変更できるようになる。クラスのインターフェースを定義する関数が同じままであれば、その実装は自由に変更できる。
カプセル化は、クレジットカード番号やパスワードなどの機密データへのアクセスを制限するためのものではなく、その目的には使用できない。カプセル化は開発ツールであり、型を簡単かつ安全に使用およびコーディングできるようにする。
保護属性
保護属性は、構造体、クラス、およびモジュールのメンバーへのアクセスを制限する。保護属性を指定するには2つの方法がある。
- 構造体またはクラスレベルで、すべての構造体またはクラスメンバーの保護を個別に指定する。
- モジュールレベルで、モジュールのすべての機能(クラス、構造体、関数、列挙型など)の保護を個別に指定する。
保護属性は、次のキーワードで指定できる。デフォルト属性はpublic
だ。
-
public
: プログラムのどの部分からも制限なくアクセス可能であることを指定する。この例としては、
stdout
がある。std.stdio
をインポートするだけで、stdout
はそれをインポートしたすべてのモジュールで使用可能になる。 -
private
: アクセスを制限する。private
クラスメンバーとモジュールメンバーは、そのメンバーを定義したモジュールからのみアクセスできる。さらに、
private
メンバー関数は、サブクラスによってオーバーライドすることはできない。 -
package
: パッケージレベルのアクセス権限を指定する。package
とマークされた機能は、同じパッケージの一部であるすべてのコードからアクセスできる。package
属性は、最も内側のパッケージのみに関係する。例えば、
animal.vertebrate.cat
モジュール内にあるpackage
定義は、vertebrate
パッケージの他のモジュールからアクセスできる。private
属性と同様に、package
メンバー関数はサブクラスでオーバーライドすることはできない。 -
protected
: 派生クラスからのアクセス権を指定する。この属性は、
private
属性を拡張する。protected
メンバーは、それを定義するモジュールだけでなく、そのprotected
メンバーを定義するクラスを継承するクラスからもアクセスできる。
さらに、export
属性は、プログラムの外側からのアクセス可能性を指定する。
定義
保護属性は3つの方法で指定できる。
単一の定義の前に記述すると、その定義のみの保護属性を指定する。これはJavaプログラミング言語に類似している:
コロンで指定すると、次の保護属性の指定まで、その後に続くすべての定義の保護属性を指定する。これは C++ プログラミング言語と類似している:
ブロックに対して指定された場合、そのブロック内のすべての定義に保護属性が適用される:
モジュールインポートはデフォルトでプライベート
import
によってインポートされたモジュールは、それをインポートしたモジュールに対してプライベートになる。間接的にそれをインポートした他のモジュールからは参照できない。例えば、school
モジュールがstd.stdio
をインポートしている場合、school
をインポートしたモジュールは、std.stdio
モジュールを自動的に使用することはできない。
school
モジュールが次の行で開始すると仮定しよう。
次のプログラムは、writeln
が参照できないためコンパイルできない:
std.stdio
そのモジュールもインポートする必要がある。
モジュールが他のモジュールを間接的に表現することが望ましい場合がある。例えば、school
モジュールが、そのユーザーのためにstudent
モジュールを自動的にインポートすることは理にかなっている。これは、import
にpublic
を指定することで実現できる。
この定義により、school
をインポートするモジュールは、student
モジュールをインポートしなくても、その中の定義を使用することができる。
上記のプログラムはschool
モジュールのみをインポートしているが、student.Student
構造体も使用可能である。
カプセル化を使用するタイミング
カプセル化により、この章の冒頭で見たような問題を回避できる。カプセル化は、オブジェクトが常に一貫した状態であることを保証するための非常に貴重なツールだ。カプセル化は、型のユーザーによるメンバーの直接的な変更からメンバーを保護することで、構造体およびクラスの不変条件を維持するのに役立つ。
カプセル化により、実装をユーザーコードから抽象化することで、実装の自由度が高まる。例えば、ユーザーがSchool.students
に直接アクセスできる場合、その配列を連想配列に変更するなどしてクラスの設計を変更することは困難である。これは、そのメンバーにアクセスしているすべてのユーザーコードに影響を与えるためだ。
カプセル化は、オブジェクト指向プログラミングの最も強力な利点の1つである。
例
カプセル化を利用して、Student
構造体とSchool
クラスを定義し、短いテストプログラムで使用しよう。
このサンプルプログラムは3つのファイルで構成される。前の章で覚えているように、school
パッケージの一部である2つのファイルは "school" ディレクトリにある。
- "school/student.d":
Student
構造体を定義するstudent
モジュール - "school/school.d":
School
クラスを定義するschool
モジュール - "deneme.d": 短いテストプログラム
以下は"school/student.d"ファイルの内容だ:
この構造体のメンバーは、同じパッケージのモジュールからのみアクセスできるように、package
とマークされている。まもなく、School
がこれらのメンバーに直接アクセスすることがわかるだろう。(これはカプセル化の原則に反すると考えるべきであることに注意。ただし、このサンプルプログラムでは、package
属性を使用することにする。)
以下の"school/school.d"モジュールは、前のモジュールを使用している。
school.student
は、school.school
のユーザーが明示的にそのモジュールをインポートする必要がないように、パブリックにインポートされている。ある意味で、student
モジュールはschool
モジュールによって利用可能になっている。School
のすべてのメンバー変数はprivateとしてマークされている。これは、このクラスのメンバー変数の整合性を保護するために重要だ。- このクラスが有用であるためには、いくつかのメンバー関数が必要である。
add()
およびtoString()
は、このクラスのユーザーが利用できる。 Student
の2つのメンバー変数はpackage
としてマークされているため、同じパッケージの一部であるSchool
はこれらの変数にアクセスできる。
最後に、これらの型を使用するテストプログラムを示す。
このプログラムは、Student
およびSchool
を、そのパブリックインターフェースを通じてのみ使用できる。これらの型のメンバー変数にはアクセスできない。その結果、オブジェクトは常に一貫性がある。
ティムは男子学生です。
女子2人、男子1人、合計3人の学生: リンジー、マーク、ナンシー
このプログラムは、School
のadd()
およびtoString()
関数によってのみSchool
と対話することに注意。これらの関数のインターフェースが同じである限り、その実装を変更しても、上記のプログラムには影響はない。