昨天介紹了繼承的缺點,以及可以怎麼用composition改寫,但是昨天的改法比較沒那麼彈性,因為Set
有多種不同資料結構的類別可以使用,上面的範例只能擴充HashSet
的功能,如果不只是要能擴充HashSet
,還要擴充其他Set
類別的功能,在設計類別的時候,constructor可以透過參數把set
儲存的類別,彈性的調整成指定的類別。以下面的範例為例,在實體化的時候傳遞參數給constructor,選擇用HashSet
或TreeSet
擴充功能。
import java.util.*;
public class InstrumentedSet<E> implements Set<E> {
// The number of attempted element insertions
private int addCount = 0;
private final Set<E> set;
public InstrumentedSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c);
}
public int getAddCount() {
return addCount;
}
@Override
public String toString() {
return set.toString();
}
public int size() {
return set.size();
}
public boolean isEmpty() {
return set.isEmpty();
}
public boolean contains(Object o) {
return set.contains(o);
}
public Iterator<E> iterator() {
return set.iterator();
}
public Object[] toArray() {
return set.toArray();
}
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}
public boolean remove(Object o) {
return set.remove(o);
}
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
public void clear() {
set.clear();
}
public static void main(String[] args) {
InstrumentedSet<String> hashInstrumentedSet = new InstrumentedSet<>(new HashSet<>());
hashInstrumentedSet.addAll(Arrays.asList("c", "d", "e"));
System.out.println(hashInstrumentedSet); // 應該輸出 [c, d, e]
InstrumentedSet<String> treeInstrumentedSet = new InstrumentedSet<>(new TreeSet<>());
treeInstrumentedSet.addAll(Arrays.asList("c", "d", "e"));
System.out.println(treeInstrumentedSet); // 應該輸出 [c, d, e]
}
}
上面的範例可以看到有非常多方法只是回傳原本方法的結果,並沒有做什麼事,如果又有一個新的類別要擴充Set
的功能,一樣實作Set
的Interface,但只更動了幾個方法,跟InstrumentedSet
會有一樣的問題,寫了很多沒有意義的程式碼,為了讓擴充功能的類別可以專注在自己要擴充的方法,可以使用Decorator pattern,用一個類別當基底,先把Set
要實作的方法回傳原本的結果,要擴充功能的類別,再去繼承這個基底,覆寫要改寫的方法就好。
而這個當基底的類別,負責把原有類別的功能forward到新的類別,也被稱為forwarding class,下面就是forwarding class的範例。
import java.util.*;
public class ForwardingSet<E> implements Set<E> {
private final Set<E> set;
public ForwardingSet(Set<E> set) { this.set = set; }
public void clear() { set.clear(); }
public boolean contains(Object o) { return set.contains(o); }
public boolean isEmpty() { return set.isEmpty(); }
public Iterator<E> iterator() { return set.iterator(); }
public Object[] toArray() { return set.toArray(); }
public <T> T[] toArray(T[] a) { return set.toArray(a); }
public int size() { return set.size(); }
public boolean add(E e) { return set.add(e); }
public boolean remove(Object o) { return set.remove(o); }
public boolean containsAll(Collection<?> c) { return set.containsAll(c); }
public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
public boolean removeAll(Collection<?> c) { return set.removeAll(c); }
public boolean retainAll(Collection<?> c) { return set.retainAll(c); }
@Override public boolean equals(Object o) { return set.equals(o); }
@Override public int hashCode() { return set.hashCode(); }
@Override public String toString() { return set.toString(); }
}
原本的InstrumentedSet
改寫之後,變得比之前簡潔易懂,因為沒有變動的方法,可以直接使用ForwardingSet
的,而這個把forwarding class再加以包裝修飾的類別,被稱為wrapper class。
import java.util.*;
public class InstrumentedSet<E> extends ForwardingSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedSet(Set<E> set) {
super(set);
}
@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 static void main(String[] args) {
InstrumentedSet<String> hashInstrumentedSet = new InstrumentedSet<>(new HashSet<>());
hashInstrumentedSet.addAll(Arrays.asList("c", "d", "e"));
System.out.println(hashInstrumentedSet); // 應該輸出 [c, d, e]
InstrumentedSet<String> treeInstrumentedSet = new InstrumentedSet<>(new TreeSet<>());
treeInstrumentedSet.addAll(Arrays.asList("c", "d", "e"));
System.out.println(treeInstrumentedSet); // 應該輸出 [c, d, e]
}
}
最後,真的需要使用繼承時,請先問問自己,兩個類別是"is-a"的關係嗎?繼承後,父類別方法的用法,跟子類別可以相容嗎?舉例來說,Properties
繼承了Hashtable
,理論上應該也可以像Hashtable
一樣,put
任何型別的key和value進去,但其實Properties
只能接受String
型別的key和value,如果推進其他型別,呼叫store
會發生不預期的行為,因為store
會把這些key和value轉成檔案,如果key和value不是String
,根本不知道輸出的結果會是什麼,所以使用繼承,必須考慮子類別是否真的是父類別的subtype,才能使用。
參考資源: