組合優於繼承

《Effective Java 中文版第2版》書中第16條中說到:java

繼承是實現代碼複用的有力手段,但它並不是永遠是完成這項工做的的最佳工具。git

組合優於繼承。github

繼承有什麼問題?

繼承打破了類的封裝性,子類依賴於父類中特定功能的實現細節。編程

繼承何時是安全的

  • 在包的內部是用繼承,不存在跨包繼承。
  • 專門爲了擴展而設計,而且具有很好的文檔說明。

一個例子

實現這樣一個HashSet,能夠跟蹤從它被建立以後曾經添加過幾個元素。安全

使用繼承實現

public class InstrumentedSet<E> extends HashSet<E> {
  // The number of attempted element insertions
  private int addCount = 0;

  public InstrumentedSet() {
  }

  public InstrumentedSet(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;
  }
}
複製代碼

類中使用 addCount 字段記錄添加元素的次數,並覆蓋父類的 add()addAll() 實現,對 addCount 字段進行設值。app

在下面的程序中,咱們指望 getAddCount() 返回3,但實際上返回的是6。ide

InstrumentedSet<String> s = new InstrumentedSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
複製代碼

問題出在於:在 HashSet 中,addAll() 的實現是基於 add() 方法的。子類在擴展父類的功能時,若是不清楚實現細節,是很是危險的,何況父類的實如今將來多是變化的,畢竟它並非爲擴展而設計的。模塊化

使用組合實現

不用擴展示有的類,而是在新的類中增長一個私有字段,引用現有類的實例。這種設計被叫作組合工具

先建立一個乾淨的 SetWrapper 組合類。ui

public class SetWrapper<E> implements Set<E> {
  private final Set<E> s;
  public SetWrapper(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(); }
}
複製代碼

SetWrapper 實現了裝飾模式,經過引用 Set<E> 類型的字段,面向接口編程,相比直接繼承 HashSet 類來得更靈活。能夠在調用該類的構造方法中傳入任意 Set 具體類。擴展該類以實現需求。

public class InstrumentedSet<E> extends SetWrapper<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;
  }
}
複製代碼

觸類旁通

注:如下代碼均是僞代碼,組合方式的實現封裝成 Android 庫並已開源,名叫 Modapter( Modular Adapter 之意)。沒錯,這裏打了個廣告。

先來看看問題。筆者曾開發的某個應用有如下2張截圖:

遊戲詳情頁面

評論列表頁面

詳情頁面和評論列表頁面均複用了評論項的實現。

評論列表頁面的 GameComentsAdapter

public class GameCommentsAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private static final int ITEM_TYPE_COMMENT = 1;
    private List<Object> mDataSet;

    @Override
    public int getItemViewType(int position) {
        Object item = getItem(position);
        if (item instanceof Comment) {
            return ITEM_TYPE_COMMENT;
        }
        return super.getItemViewType(position);
    }

    protected Object getItem(int position) {
        return mDataSet.get(position);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == ITEM_TYPE_COMMENT) {
            View itemView = inflater.inflate(R.layout.item_comment, parent, false);
            return new CommentViewHolder(itemView);
        }
        return null;
    }

    @Override
    public int getItemCount() {
        return mDataSet.size();
    }
}
複製代碼

if-else 方式實現

修改 GameComentsAdapter 類,增長對遊戲詳情項的適配支持。

public class GameCommentsAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private static final int ITEM_TYPE_COMMENT = 1;
    private static final int ITEM_TYPE_GAME_DETAIL = 2;
    private List<Object> mDataSet;

    @Override
    public int getItemViewType(int position) {
        Object item = getItem(position);
        if (item instanceof Comment) {
            return ITEM_TYPE_COMMENT;
        }
        if (item instanceof GameDetail) {
            return ITEM_TYPE_GAME_DETAIL;
        }
        return super.getItemViewType(position);
    }

    protected Object getItem(int position) {
        return mDataSet.get(position);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        if (viewType == ITEM_TYPE_COMMENT) {
            View itemView = inflater.inflate(R.layout.item_comment, parent, false);
            return new CommentViewHolder(itemView);
        }
        if (viewType == ITEM_TYPE_GAME_DETAIL) {
            View itemView = inflater.inflate(R.layout.item_game_detail, parent, false);
            return new GameDetailViewHolder(itemView);
        }
        return null;
    }

    @Override
    public int getItemCount() {
        return mDataSet.size();
    }
}
複製代碼

在遊戲詳情頁面爲 RecyclerView 建立一個 GameCommentsAdapter 對象。但該方式會讓 GameCommentsAdapter 變得臃腫,也不知足OCP開閉原則。

繼承方式實現

擴展一個 Adapter 至少要實現 getItemViewType()onCreateViewHolder() 等方法,爲了複用 GameComentsAdapter 類中對評論項,詳情頁面的 GameDetailAdapter 繼承該類。

class GameDetailAdapter extends GameCommentsAdapter {
    private static final int ITEM_TYPE_GAME_DETAIL = 2;

    @Override
    public int getItemViewType(int position) {
        Object item = getItem(position);
        if (item instanceof GameDetail) {
            return ITEM_TYPE_GAME_DETAIL;
        }
        return super.getItemViewType(position);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType == ITEM_TYPE_GAME_DETAIL) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            View itemView = inflater.inflate(R.layout.item_game_detail, parent, false);
            return new GameDetailViewHolder(itemView);
        }
        return super.onCreateViewHolder(parent, viewType);
    }
}
複製代碼

忽然來了一個新需求

產品但願在詳情頁面添加推薦項,複用首頁列表項,以下圖所示:

首頁

實現效果以下圖所示:

詳情頁面增長

Java 是單繼承的,GameDetailAdapter 已經繼承了 GameComentsAdapter 類了,沒法再繼承 HomeAdapter

難道繼續在 GameComentsAdapter 類中增長 if 判斷?

組合方式

爲了方便閱讀,部分代碼已省略。

定義一個模塊化的適配器 Adapter 類,爲了可以被以上 Adapter 類所管理,數據項和視圖項須要作一些配合:前者繼承 AbstractItem 類,後者須要繼承 ItemViewHolder 類。

class Comment extends AbstractItem {}
class GameDetail extends AbstractItem {}
class Game extends AbstractItem {}

class CommentViewHolder extends ItemViewHolder<Comment> {}
class GameDetailViewHolder extends ItemViewHolder<GameDetail> {}
class GameViewHolder extends ItemViewHolder<Game> {}
複製代碼

AbstractItem 類定義了一個 type 屬性,表明數據項的類型,會與經過註冊的數據項配置信息進行比對,當 type 屬性值同樣時,就會爲該數據項 AbstractItem 建立對應的視圖項 ViewHolder

若是由於 Java 單繼承的關係沒法繼承 AbstractItem 類,能夠選擇實現 Item 接口,實現如下方法。

public interface Item {

    void setType(int type);

    int getType();
}
複製代碼

此時,數據項和視圖項的準備工做已完成,接下來能夠組合它們實現需求。

在評論列表頁面,建立一個 Adapter 實例,並添加評論項功能。

List<Item> dataSet = new ArrayList<>();
dataSet.add(new Comment());
dataSet.add(new GameDetail());

Adapter adapter = new Adapter();
adapter.getManager()
        .register(ITEM_TYPE_COMMENT, CommentViewHolder.class)
        .register(ITEM_TYPE_GAME_DETAIL, GameDetailViewHolder.class)
        .setList(dataSet);
複製代碼

在遊戲詳情頁面,建立一個 Adapter 實例,並添加遊戲項功能。

List<Object> dataSet = new ArrayList<>();
dataSet.add(new Comment());
dataSet.add(new GameDetail());
dataSet.add(new Game());

Adapter adapter = new Adapter();
adapter.getManager()
        .register(ITEM_TYPE_COMMENT, CommentViewHolder.class)
        .register(ITEM_TYPE_GAME_DETAIL, GameDetailViewHolder.class)
        .register(ITEM_TYPE_GAME, GameViewHolder.class)
        .setList(dataSet);
複製代碼

當某個頁面再也不支持評論項時,咱們只要刪除如下代碼便可,不會修改到其餘地方,知足OCP設計原則。

dataSet.add(new Comment());
adapter.getManager().unregister(ITEM_TYPE_COMMENT);
複製代碼

實現原理

引入 ItemManager 接口,統一管理項數據、註冊和註銷視圖項配置信息。

public interface ItemManager {

    ItemManager setList(List<? extends Item> list);

    <T extends ViewHolder> ItemManager register(int type, Class<T> holderClass);

    <T extends ViewHolder> ItemManager register(int type, @LayoutRes int layoutId, Class<T> holderClass);
    
    ItemManager register(ItemConfig config);

    ItemManager unregister(int type);
    
    <T extends Item> T getItem(int position);
}
複製代碼

該接口的實現類是 AdapterDelegate,主要實現了getItemViewTypeonCreateViewHolder, onBindViewHolder 三個 if-else 重災區方法。

public final class AdapterDelegate implements ItemManager {

    public int getItemViewType(int position) {
        Item item = getItem(position);
        ItemConfig adapter = null;
        if (item != null) {
            adapter = registry.get(item.getType());
        }
        if (adapter == null) {
            // TODO
            return 0;
        }
        return adapter.getType();
    }

    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ItemConfig adapter = registry.get(viewType);
        if (adapter == null) {
            return null;
        }

        int layoutId = adapter.getLayoutId();
        layoutId = layoutId == 0 ? adapter.getType() : layoutId;
        if (layoutId > 0) {
            View itemView = LayoutInflater.from(parent.getContext())
                    .inflate(layoutId, parent, false);
            return createViewHolder(itemView, adapter.getHolderClass());
        }

        return null;
    }

    @SuppressWarnings("unchecked")
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Item item = getItem(position);
        if (holder instanceof ItemViewHolder) {
            ItemViewHolder viewHolder = (ItemViewHolder) holder;
            viewHolder.setItem(item);
            viewHolder.onViewBound(item);
        }
    }
}
複製代碼

使用 AdapterDelegate 實現惟一的 Adapter,將主要的代碼委託給前者。

public class Adapter extends RecyclerView.Adapter<ViewHolder> {

    private AdapterDelegate delegate = new AdapterDelegate();

    @Override
    public int getItemViewType(int position) {
        return delegate.getItemViewType(position);
    }

    @NonNull
    @Override
    public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return delegate.onCreateViewHolder(parent, viewType);
    }

    @Override
    public final void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        delegate.onBindViewHolder(holder, position);
    }
    
    public ItemManager getManager() {
        return delegate;
    }
}
複製代碼

延伸

在 Java 生態圈以外,有很多組合優於繼承的實踐。

Kotlin

Kotlin 語言有 delegation 機制,能夠方便開發者使用組合。

interface Base {
   fun print()
}

class BaseImpl(val x: Int) : Base {
   override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
   val b = BaseImpl(10)
   Derived(b).print()
}
複製代碼

Kotlin 版 InstrumentedHashSet

class InstrumentedHashSet<E>(val set: MutableSet<E>)
    : MutableSet<E> by set {

    private var addCount : Int = 0

    override fun add(element: E): Boolean {
        addCount++
        return set.add(element)
    }

    override fun addAll(elements: Collection<E>): Boolean {
        addCount += elements.size
        return set.addAll(elements)
    }
}
複製代碼

Go

Go 語言沒有繼承機制,經過原生支持組合來實現代碼的複用。如下分別是 ReaderWriter 接口定義。

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}
複製代碼

經過組合能夠定義出具有讀取和寫入的新類型。

type ReadWriter interface {
	Reader
	Writer
}
複製代碼

上述的例子是接口組合,也能夠是實現組合。(下面的例子來自 Go in Action 一書)

type user struct {
	name  string
	email string
}

// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

// admin represents an admin user with privileges.
type admin struct {
	user  // Embedded Type
	level string
}

// main is the entry point for the application.
func main() {
	// Create an admin user.
	ad := admin{
		user: user{
			name:  "john smith",
			email: "john@yahoo.com",
		},
		level: "super",
	}

	// We can access the inner type's method directly.
	ad.user.notify()

	// The inner type's method is promoted.
	ad.notify()
}
複製代碼

推薦書籍

參考資料

相關文章
相關標籤/搜索