Javaには皆さんご存知の通り、継承を使用できます。
コードのリサイクルを手伝う強力なツールです。
しかし、継承を適切に使ってない場合、コードは壊れやすいです。
このため、継承にはそれに見合った文書があるべきです。
このため、継承にはそれに見合った文書があるべきです。
イペクチブJavaでは、まず、継承の欠点をならべています。
まず、メソッドとは異なり、継承はカプセル化の原則に守っていません。
上位クラスを継承したサブクラスは、上位クラスに依存します。
上位クラスを継承したサブクラスは、上位クラスに依存します。
上位クラスが変わった場合、サブクラスのコードも必ず修正する必要があります。
これはカプセル化の条件を破る形に繋がります。
下はサンプルコードです。
package dao;
import java.util.Collection;
import java.util.HashSet;
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet(){}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
HashSetを継承するクラスです。
このクラスのコードを下記のように実装して実行してみましょう。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
機体された結果は3です。
なぜこのような違いが生じるかは下の絵をご覧ください。
ここで大事な処は、2番目と最後のステップです。
HashSetのaddAllメソッドは、addメソッドを使用して実装されます。
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
これらの事実は、HashSetのマニュアルに載ってありません。
解決策としてInstrumentedHashSetのaddAllメソッドを削除すると、正常に動作します。
しかし、上位クラスのaddAllを使用するということ自体が、
依存していることを意味します。
つまり、カプセル化とは離れて行きます。
新しいメソッドの作成を思い浮かぶかも知れません。
しかし、もし新しいくリリースされ、メソッドが追加されたとき
偶然にメソッドのシグネチャ(名前+パラメータ)が、サブクラスと同じ場合
そのサブクラスは、コンパイルエラーの問題に直面します。
つまり、Override失敗になります。
リリースされてクラスが少しでも変更された場合
マニュアルが細かく記していない場合などの状況に
継承は壊れやすいクラスになります。
このような問題を解決する方法が一つあります。
「継承」の代わりの「構成パターン」を使用する方法です。
EFJで紹介したコードを一度みましょう。
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
public boolean contains(Object o) {
return s.contains(o);
}
public boolean isEmpty() {
return s.isEmpty();
}
public int size() {
return s.size();
}
public Iterator<E> iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
上記のコードは、Setを囲んでいるのでWrapperクラス
下のコードがforwardingクラスです。
InstrumentedSetクラスは、Setインタフェースを実装し、
Setオブジェクトをパラメータとして受け取るコンストラクタを持ちます。
本来、継承を使ったアプローチは、一クラスだけ適用が可能で
上位クラスごとに別々のコンストラクタを実装してくれる必要がありますが、
Wrapperクラスの技術を使うと、いかなるSetも必要に応じて修正が可能で
すでに存在するコンストラクタもそのまま使用することができます。
下のコードがforwardingクラスです。
InstrumentedSetクラスは、Setインタフェースを実装し、
Setオブジェクトをパラメータとして受け取るコンストラクタを持ちます。
本来、継承を使ったアプローチは、一クラスだけ適用が可能で
上位クラスごとに別々のコンストラクタを実装してくれる必要がありますが、
Wrapperクラスの技術を使うと、いかなるSetも必要に応じて修正が可能で
すでに存在するコンストラクタもそのまま使用することができます。
Set<Date> s = new InstrumentedSet<Sate>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
.
構成パターンは、欠点があまりありませんが、
コールバックのパターンでは、使い難いです
コールバックは、自分のアドレスを他のオブジェクトに渡し、
後で呼び出される形のパターンです。
ここで問題は、オブジェクトは自分が包まれるオブジェクトであることを知らないことです。
このため、コールバックを使用する時、構成パターンは使っちゃいけません。
コールバックのパターンでは、使い難いです
コールバックは、自分のアドレスを他のオブジェクトに渡し、
後で呼び出される形のパターンです。
ここで問題は、オブジェクトは自分が包まれるオブジェクトであることを知らないことです。
このため、コールバックを使用する時、構成パターンは使っちゃいけません。
댓글 없음:
댓글 쓰기