组合优于继承

栏目: Android · 发布时间: 6年前

内容简介:实现这样一个类中使用在下面的程序中,我们期望
  • 在包的内部是用继承,不存在跨包继承。
  • 专门为了扩展而设计,并且具备很好的文档说明。

一个例子

实现这样一个 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 库并开源,敬请期待。没错,这里打了个广告。

笔者曾开发的某个应用有以下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 判断?

组合方式

先把各个列表项的创建、绑定等抽离出来,引入 Subadapter 类。

interface Subadapter {
    boolean isFor(Object item);
    int getLayoutId();
    BaseViewHolder onCreateViewHolder(View itemView);
}
复制代码

实现 CommentSubadapter

class CommentSubadapter implements Subadapter {

    @Override
    public boolean isFor(Object item) {
        return item instanceof Comment;
    }

    @Override
    public int getLayoutId() {
        return R.layout.item_comment;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(View itemView) {
        return new CommentViewHolder(itemView);
    }
}
复制代码

实现 GameDetailSubadapter

class GameDetailSubadapter implements Subadapter {

    @Override
    public boolean isFor(Object item) {
        return item instanceof GameDetail;
    }

    @Override
    public int getLayoutId() {
        return R.layout.item_game_detail;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(View itemView) {
        return new GameDetailViewHolder(itemView);
    }
}
复制代码

实现 SubadapterManager

class SubadapterManager {

    private SparseArray<Subadapter> mSubadapters = new SparseArray<>();

    public void addSubadapter(int itemType, Subadapter subadapter) {
        mSubadapters.put(itemType, subadapter);
    }

    public int getItemViewType(Object item) {
        for (int i = 0; i < mSubadapters.size(); i++) {
            Subadapter subadapter = mSubadapters.get(i);
            if (subadapter.isFor(item)) {
                return mSubadapters.keyAt(i);
            }
        }
        return 0;
    }

    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int type) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        Subadapter subadapter = mSubadapters.get(type);
        View itemView = inflater.inflate(subadapter.getLayoutId(), parent, false);
        return subadapter.onCreateViewHolder(itemView);
    }
}
复制代码

实现唯一的 Adapter

class Adapter extends RecyclerView.Adapter<BaseViewHolder> {
    private List<Object> mDataSet;
    private SubadapterManager mSubadapterManager;

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

    public void addSubadapter(int itemType, Subadapter subadapter) {
        mSubadapterManager.addSubadapter(itemType, subadapter);
    }

    @Override
    public int getItemViewType(int position) {
        Object item = getItem(position);
        return mSubadapterManager.getItemViewType(item);
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return mSubadapterManager.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
        holder.bind(getItem(position));
    }

    @Override
    public int getItemCount() {
        return mDataSet.size();
    }

    public void setDataSet(List<Object> set) {
        mDataSet = set;
    }
}
复制代码

组合起来使用。在评论列表页面,创建一个 Adapter 实例,并添加评论项功能。

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

Adapter adapter = new Adapter();
adapter.addSubadapter(ITEM_TYPE_COMMENT, new CommentSubadapter());
adapter.addSubadapter(ITEM_TYPE_GAME_DETAIL, new GameDetailSubadapter());
adapter.setDataSet(dataSet);
复制代码

实现 GameSubadapter 完成新需求。

class GameSubadapter implements Subadapter {
    @Override
    public boolean isFor(Object item) {
        return item instanceof Game;
    }

    @Override
    public int getLayoutId() {
        return R.layout.item_game;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(View itemView) {
        return new GameViewHolder(itemView);
    }
}
复制代码

在游戏详情页面,创建一个 Adapter 实例,并添加游戏项功能。

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

Adapter adapter = new Adapter();
adapter.addSubadapter(ITEM_TYPE_COMMENT, new CommentSubadapter());
adapter.addSubadapter(ITEM_TYPE_GAME_DETAIL, new GameDetailSubadapter());
adapter.addSubadapter(ITEM_TYPE_GAME, new GameSubadapter());
adapter.setDataSet(dataSet);
复制代码

当某个页面不再支持评论项时,我们只要删除以下代码即可,不会修改到其他地方,满足OCP设计原则。

dataSet.add(new Comment());
adapter.addSubadapter(ITEM_TYPE_COMMENT, new CommentSubadapter());
复制代码

延伸

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()
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

从0到1

从0到1

彼得·蒂尔、布莱克·马斯特斯 / 高玉芳 / 中信出版股份有限公司 / 2015-1-1 / CNY 45.00

图书简介: http://v.youku.com/v_show/id_XOTA0NjcyMzE2.html?wm=3333_2001 硅谷创投教父、PayPal创始人作品,斯坦福大学改变未来的一堂课,为世界创造价值的商业哲学。 在科技剧烈改变世界的今天,想要成功,你必须在一切发生之前研究结局。 你必须找到创新的独特方式,让未来不仅仅与众不同,而且更加美好。 从0到1,......一起来看看 《从0到1》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试