《Effective Java 中文版第2版》書中第16條中說到:
繼承是實現程式碼複用的有力手段,但它並非永遠是完成這項工作的的最佳工具。
組合優於繼承。
繼承有什麼問題?
繼承打破了類的封裝性,子類依賴於父類中特定功能的實現細節。
繼承什麼時候是安全的
- 在包的內部是用繼承,不存在跨包繼承。
- 專門為了擴充套件而設計,並且具備很好的文件說明。
一個例子
實現這樣一個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
欄位進行設值。
在下面的程式中,我們期望 getAddCount()
返回3,但實際上返回的是6。
InstrumentedSet<String> s = new InstrumentedSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
複製程式碼
問題出在於:在 HashSet
中,addAll()
的實現是基於 add()
方法的。子類在擴充套件父類的功能時,如果不清楚實現細節,是非常危險的,況且父類的實現在未來可能是變化的,畢竟它並不是為擴充套件而設計的。
使用組合實現
不用擴充套件現有的類,而是在新的類中增加一個私有欄位,引用現有類的例項。這種設計被叫做組合。
先建立一個乾淨的 SetWrapper
組合類。
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
,主要實現了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
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 語言沒有繼承機制,通過原生支援組合來實現程式碼的複用。以下分別是 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()
}
複製程式碼