変更不能オブジェクトの使用
データオブジェクトに対して、その値が変更できる状態にしておくと、データの変更を制作者の意図しないタイミングで行われてしまい、データの整合性が取れなくなってしまう場合がある。
これを防止するために、変更不能オブジェクトという考え方が存在する。
最も簡単な実装としては、引数の指定に「final」修飾子を加えることがある。public void hogehoge(final String arg){ arg = new String("piyopiyo"); }このようなコーディングをすると、コンパイル時にエラーが出る。引数argが保護されているからである。ただし、これは「argと言う参照の値」が変更不能となっているだけで、argの参照先には関与できない問題がある。例えば、
public void hogehoge(final Stringbuffer arg){ arg.append("hogehoge"); // コンパイラ素通り arg = new Stringbuffer("piyopiyo"); // これは怒られる }と、なってしまう。以上と異なり、オブジェクト側で変更不能としようとするのが次の二つのTIP、immutableオブジェクトと値参照専用インターフェースである。
この問題に対してもう一つの解答がある。
値が変更されることで問題が起きるのであれば、値が変更されても問題が起きないようにする、という考え方である。具体的には、オブジェクトへの参照を渡すときに、オブジェクトの実体への参照ではなく、オブジェクトのクローンへの参照を渡すようにする。当然データオブジェクトがフィールドでほかのオブジェクトへの参照を持っているようであれば、「深いコピー」を実装したクローンの提供方法を考えなければいけない。
さらに、「変更不能なクローンオブジェクト」という考え方もあるのだが、これは省略。
変更不能オブジェクト:immutableオブジェクト
特徴
値の変更を可能とするメソッドを提供しないデータオブジェクトのこと。以下の操作を行うことで実現できる。
欠点
- フィールドをすべてprivateにする。
- 値のセットはコンストラクタでのみ行う。
- getterのみ提供しsetterは提供しない。
- newは重いから頻繁に更新を行うものには不向き
当然ながら、setのコストよりもnewのコストが断然大きいのでなるべくなら一度newしたら内容が変わらないようなオブジェクトに使用するべきである。setを頻繁に行うようであれば、次の値参照専用インターフェースの使用を勧める。
さらに、他のオブジェクトから参照されるようなものであれば、致命的である。
newした時点でオブジェクトの主記憶上の位置が変わってしまうので、それまで使われていた参照の値が無効となり、参照の張り直しが必須になる。
変更不能オブジェクト:値参照専用インターフェース
特徴
データオブジェクトに対して、読みとりの機能をのみ実装する事を宣言したインターフェースを実装する。
- データオブジェクトそのものはsetterを含めフル実装。
- getter系メソッドの頭書きを記述したインターフェースを用意し、実装する。
- データを必要とする他のオブジェクトに対してはインターフェース経由で引き渡す。
- (値変更の必要がなければ)インターフェースのみ公開する。
この操作により、もしsetter系のメソッドが呼ばれたとしても、インターフェースがサポートしていないためコンパイラでチェックが入り、値の変更を防止できる。
欠点
- インターフェースを経由しないで使用された場合、無防備。
こればっかりは致し方ない。ドキュメントでしっかりとインターフェース経由で使用するように厳命しておきましょう。
サンプルプログラムpublic interface HogeObjectInterface{ public int getPiyo(); } public class HogeObject implements HogeObjectInterface{ private int piyo; public HogeObject(int piyo){ this.piyo = piyo; } public int getPiyo(){ return piyo; } public void setPoyo(int piyo){ this.piyo = piyo; } } public class test{ public void foo(HogeObjectInterface hogeObject){ // インターフェースを渡す。 int piyo = hogeObject.getPiyo(); // hogeObject.setPiyo(piyo++); // これはコンパイルエラー } }
継承に関すること
継承はクラスAとクラスBとの関係が「A is a B」の時に用いる。
データクラスを機能拡張するために継承するのは無駄。機能拡張したければユーティリティークラスを作って、そちらで処理を行うべき。
なぜならば・・・データクラスのバージョンアップの問題。ポリモルフィズムの問題。etc...
オブジェクトのクラスは変更不可である。 このため、オブジェクトの種類が変わるようであればその種類に継承でクラスを与えるようなことはすべきではない。
例・・・携帯の料金形態 学生と社会人。学生から社会人になった場合に対応不可になってしまう。
動的に選択されるのはインスタンスメソッドのみ。
サブクラスで同名のメンバーを宣言した場合、動作は「隠蔽」になり、オーバーライドしない。スーパークラスの参照にサブクラスを代入すると、インスタンスはスーパー側を見てしまう=コンパイル時に静的に決まる。class Super{ public String field = "Super class"; } class Child extends Super{ public String field = "Child class"; } public class Test{ public static void main(String argv[]){ Super object1 = new Super(); Child object2 = new Child(); Super object3 = new Child(); System.out.println(object1.field); System.out.println(object2.field); System.out.println(object3.field); } } 実行結果 >Super class >Child class >Super class
メソッドのオーバーライドをした場合アクセス制御を広げることはできるが狭めることはできない。(引数の処理を考えれば当たり前)
同様に、例外は増やすことはできない。
- spaghetti inheritance(スパゲティ継承)[Smalltalkなど継承を利用するオブジェクト指向言語のユーザーの間で使われる]
- n.渦巻状の複雑なクラス-サブクラス継承関係。コードを再利用するためだけに別のクラスから安易にサブクラスを派生させたのが原因でできあがることが多い。スパゲティコード(spaghetti code)と同罪だとして、こういう行為を思いとどまらせるようという意図が込められた造語(功を奏している)。
---『The New Hacker's Dictionary ハッカーズ大辞典』より
アクセス制御
同一クラスであれば、インスタンスが異なっていてもprivteメンバーにアクセスできる。
状態付き例外の使用
パッケージの例外は極少数にしてその例外のインスタンス内で原因等の状態を保持するclass HogePackageException extends Exception { public static final int HOGE_HOGE_EXCEPTION = 1; public static final int PIYO_PIYO_EXCEPTION = 2; private String message; private int errorCode; //ゲッターとセッター } try{ // 何らかの処理 }catch ( HogePackageException ex ){ if( ex.getErrorCode() == HogePackageException.HOGE_HOGE_EXCEPTION ){ } else if( ex.getErrorCode() == HogePackageException.PIYO_PIYO_EXCEPTION ){ } else { throw ex; // 処理できないので例外を投げ直す } }最後でthrowを忘れると例外をもみ消すことになるから注意。
例外ディスパッチャ
例外を渡すと処理してくれるユーティリティークラスの作成。状態付き例外と併用することでさらにコードが読みやすくなる。ただし、例外の種類によって一括の処理を行ってしまうため、ある例外の種類のある特定の場面で起きた場合には特殊なことをしたい、といったときには、ディスパッチャがあるにもかかわらず使えないと言うことになってしまう事もあり得る。
一応、例外の種類の作り方を構造的に行うことで回避は可能。