iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
Software Development

深入淺出Java 30天系列 第 21

Day 21: 最好使用composition而不是繼承(下)

  • 分享至 

  • xImage
  •  

昨天介紹了繼承的缺點,以及可以怎麼用composition改寫,但是昨天的改法比較沒那麼彈性,因為Set有多種不同資料結構的類別可以使用,上面的範例只能擴充HashSet的功能,如果不只是要能擴充HashSet,還要擴充其他Set類別的功能,在設計類別的時候,constructor可以透過參數把set儲存的類別,彈性的調整成指定的類別。以下面的範例為例,在實體化的時候傳遞參數給constructor,選擇用HashSetTreeSet擴充功能。

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,才能使用。

參考資源:


上一篇
Day 20: 最好使用composition而不是繼承(上)
下一篇
Day 22: 設計並記錄繼承的使用方式,否則禁止使用繼承
系列文
深入淺出Java 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言