Home 组合优于继承
Post
Cancel

组合优于继承

《Effective Java 中文版第2版》书中第16条中说到:

继承是实现代码复用的有力手段,但它并非永远是完成这项工作的的最佳工具。

继承有什么问题?

继承打破了类的封装性,子类依赖于父类中特定功能的实现细节。

继承什么时候是安全的

  • 在包的内部是用继承,不存在跨包继承。
  • 专门为了扩展而设计,并且具备很好的文档说明。

一个例子

实现这样一个HashSet,可以跟踪从它被创建之后曾经添加过几个元素。

使用继承实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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。

1
2
InstrumentedSet<String> s = new InstrumentedSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));

问题出在于:在 HashSet 中,addAll() 的实现是基于 add() 方法的。子类在扩展父类的功能时,如果不清楚实现细节,是非常危险的,况且父类的实现在未来可能是变化的,毕竟它并不是为扩展而设计的。

使用组合实现

不用扩展现有的类,而是在新的类中增加一个私有字段,引用现有类的实例。这种设计被叫做组合

先创建一个干净的 SetWrapper 组合类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 具体类。扩展该类以实现需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 类,增加对游戏详情项的适配支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 继承该类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 类。

1
2
3
4
5
6
7
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 接口,实现以下方法。

1
2
3
4
5
6
public interface Item {

    void setType(int type);

    int getType();
}

此时,数据项和视图项的准备工作已完成,接下来可以组合它们实现需求。

在评论列表页面,创建一个 Adapter 实例,并添加评论项功能。

1
2
3
4
5
6
7
8
9
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 实例,并添加游戏项功能。

1
2
3
4
5
6
7
8
9
10
11
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设计原则。

1
2
dataSet.add(new Comment());
adapter.getManager().unregister(ITEM_TYPE_COMMENT);

实现原理

引入 ItemManager 接口,统一管理项数据、注册和注销视图项配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 重灾区方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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,将主要的代码委托给前者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 机制,可以方便开发者使用组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 接口定义。

1
2
3
4
5
6
7
type Reader interface {
	Read(p []byte) (n int, err error)
}

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

通过组合可以定义出具备读取和写入的新类型。

1
2
3
4
type ReadWriter interface {
	Reader
	Writer
}

上述的例子是接口组合,也可以是实现组合。(下面的例子来自 Go in Action 一书)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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()
}

推荐书籍

参考资料

This post is licensed under CC BY 4.0 by the author.