Revised: May/5th/2008: Since: Dec./26th/2001
関係の深い属性とその振る舞いを一つのクラスにまとめることをカプセル化(encapsulation)と呼びます。また、責務に照らして外部に公開しないでもよいものを隠して保護することを情報隠蔽(information hiding)と呼びます。
自分の状態の面倒を見る範囲を責務と呼びます。クラスが適切な責務を帯びているかどうかは、別のプログラムに持っていっても動作するかどうかを考えてみることです。ころっとしたモノのように、別のコンテキストに置いても正しく動作するようであれば適切です。パッケージについても同様です。
カプセル化では、クラス間の依存関係が極小化されるようにします。複数の役割を帯びるクラスや、役割が重複する複数のクラスが現れたら、クラスの切り分け方を見直す必要があります。隠蔽では、クラスの役割に照らして、必要最小限の良く定義されたインタフェース部分だけを公開して、あとは全て実装の詳細/変更するかもしれない内部として隠蔽します。これを、凝集度(cohesion)の向上と結合度(coupling)の低下と呼びます。
そのためには、オブジェクトが、外部から見たときに、どういう状態を取りうるもので、どのように振る舞うべきかを定義します。そして、外部に公開しないでも良い、内部の動作の詳細は全て隠蔽します。逆に言うと、最初は全て隠蔽して、外部とのやり取りに使う必要最低限のものだけを公開します。
最初に隠蔽するのはクラスです。パッケージの機能として外部に公開する必要のあるクラスだけをpublic修飾して、それ以外はパッケージ内部の処理でだけ利用するデフォルト・アクセスにしておきます。
次に隠蔽するのがメンバ変数です。全ての変更可能なメンバ変数はprivateにしてクラスの外部から隠蔽します。メンバ変数の変更や読み取りは、クラスの外部に公開されたメソッドを通して行うようにします。フィールドにアクセスするためのメソッドをアクセッサーと呼び、値を読み取るものをゲッター、更新するものをセッターと呼びます。セッターには、フィールドに不正な値をセットしないためのロジックを実装して、必要最低限のアクセス修飾子で公開します。
カプセル化の観点では、アクセッサーを実装するときは、次のことに注意する必要があります。
その他のメソッドについても、アクセス範囲に注意する必要があります。原則として、全てのメソッドはprivateであるべきで、クラスの責務として必要最小限のものだけ外部に公開します。
ここではフィールド変数を直接アクセス obj.y しています。カプセル化という観点では、フィールドは private 宣言しておき、メソッドを介してアクセスしたほうが、変数値を保護できて安全です。
CapsuleTest.java:
class Capsule {
private int y;
void setY(int x) {
y = x;
}
int getY() {
return y;
}
void method() {
y *= 5;
System.out.println("----method()----");
System.out.println("method() y: " + y);
}
}
class CapsuleTest {
public static void main(String[] args) {
//インスタンス化
Capsule obj = new Capsule();
//フィールドにアクセス
obj.setY(10);
//メソッド呼び出し
obj.method();
System.out.println("----main()------");
System.out.println(" main() y: " + obj.getY());
}
}
フィールド y は private 修飾されているので、他のクラスからはアクセスできません。他のクラスからのアクセスのために、ゲット/セットのメソッドを作りました。
フィールドはオブジェクトの属性としてメモリ上に保持されつづける変数です。他のオブジェクトからアクセスされるために、おかしな値がセットされる危険が伴います。クラス開発者の責任として、予めおかしな値がセットされないように、ロジックを組んでおくことが推奨されますが、そのためにフィールドを private 宣言しておき、アクセスするにはメソッドを介するようにします。他のパッケージからのアクセスも許可する場合は、これらのメソッドを public 宣言しておきましょう。
値を代入するメソッドをセッター (setter) 、取得するメソッドをゲッター (getter) と呼び、両方あわせてアクセッサー・メソッドと呼びます。フィールド xyz のセッターは setXyz()、ゲッターは getXyz() と命名することが多いようです。
良く見かけるコードに、メンバ変数を全てprivateにし、全てのメンバ変数にpublicなゲッターとセッターを用意しているものがあります。引数をメンバ変数に代入するだけのセッターと、メンバ変数をreturnするだけのゲッターであれば、メンバ変数がpublicに公開されているのと大差ありません。
せっかく保護するのだから、ロジックにおいても保護すべきです。次の例は、メンバー変数をprivate修飾して保護しています。ここでは、コンストラクタとセッターにフィールドの状態を判定する条件分岐をつけて、違反する場合は例外をスローするようにしています。いずれの場合も、変数の状態がnullである場合は、呼び出し元にIllegalArgumentException型例外をスローします。
public class MyClassDemo {
// インスタンス変数
private String msg, name;
// コンストラクタ
public MyClassDemo(String myName) {
if (myName == null) {
throw new IllegalArgumentException("myName is null.");
}
name = myName;
}
// セッター
public void setMsg(String myMsg) {
if (myMsg == null) {
throw new IllegalArgumentException("myMsg is null.");
}
msg = myMsg;
}
// ゲッター
public String getMsg() {
if (msg == null) {
setMsg("No Message, so far.");
}
return name + ": " + msg;
}
}
メンバ変数に代入されているオブジェクトを返すときにも注意が必要です。変更可能なオブジェクトの場合、返した先のメソッドでオブジェクトが変更されると、メンバ変数から参照されるオブジェクトのが変更されることになります。オブジェクト指向としては当然の動作ですが、特にマルチスレッドで複数のコードから同時にアクセスされる場合は、整合性が破壊される恐れがあります。このときは、メンバ変数が参照するオブジェクトとは別のオブジェクトに値をコピーして返す必要があります。
次の例は、メンバ変数に保持した変更可能なStringBuffer型オブジェクトに対して、新たに変更不能なString型オブジェクトを生成して返している例です。
class DefensiveCopyDemo {
private StringBuffer strBuf = new StringBuffer();
void setBuffer(String str) {
strBuf.append(str);
}
String getMessage() {
return new String(strBuf);
}
}
ここでは、コンストラクタ引数に渡しましたが、新しい空のオブジェクトを生成して、メンバ変数を逐次的にコピーする方法もあります。
パッケージ java.util のクラス Collections では、可変オブジェクトを不変オブジェクトでラップするメソッド unmodifiedXxxx() が用意されています。
インタフェース Cloneable を実装するクラスでは、オブジェクトを複製するためのメソッド clone() を利用できます。