《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();
}
}
複製代碼
修改 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
,主要實現了getItemViewType
,onCreateViewHolder
, 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 語言有 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()
}
複製代碼
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 語言沒有繼承機制,經過原生支持組合來實現代碼的複用。如下分別是 Reader
和 Writer
接口定義。
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()
}
複製代碼