定义
允许将对象组成树形结构来表现“整体-部分”的层次结构。组合能让客户以一致的方式处理个别对象和对象组合。属于结构型设计模式。
使用场景
需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
结构
模式所涉及的角色有:
抽象构件角色(component):是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
这个接口可以用来管理所有的子对象。(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
树叶构件角色(Leaf):在组合树中表示叶节点对象,叶节点没有子节点。并在组合中定义图元对象的行为。
树枝构件角色(Composite):定义有子部件的那些部件的行为。存储子部件。在Component接口中实现与子部件有关的操作。
客户角色(Client):通过component接口操纵组合部件的对象。
实现
通过前边的使用场景说明,我们发现文件结构正好就是一个整体-部分层次的树形结构。那如果我们想知道文件名,我们好像并不需要管他是文件还是文件夹,所以我们要想办法忽略不同层次对象的差异。接下来我们看下实现:
抽象构件角色,即文件类的抽象接口
/**
* 文件抽象类
*/
public abstract class File {
private String name;
public File(String name) {
this.name = name;
}
//操作方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void watch();
//组合方法
public void add(File file) {
throw new UnsupportedOperationException();
}
public void remove(File file) {
throw new UnsupportedOperationException();
}
public File getChild(int position) {
throw new UnsupportedOperationException();
}
}
树叶构件角色,即我们的具体格式文件,比如文本文件
/**
* 文本文件
*/
public class TextFile extends File {
public TextFile(String name) {
super(name);
}
@Override
public void watch() {
Log.e("组合模式", "这是一个叫" + getName() + "文本文件");
}
}
树枝构件角色,即我们的文件夹
/**
* 文件夹类
*/
public class Folder extends File {
private List
mFileList; public Folder(String name) { super(name); mFileList = new ArrayList<>(); } @Override public void watch() { StringBuffer fileName = new StringBuffer(); for (File file : mFileList) { fileName.append(file.getName() + ";"); } Log.e("组合模式", "这是一个叫" + getName() + "文件夹,包含" + mFileList.size() + "个文件,分别是:" + fileName); } @Override public void add(File file) { mFileList.add(file); } @Override public void remove(File file) { mFileList.remove(file); } @Override public File getChild(int position) { return mFileList.get(position); } }
客户端
/**
* 测试组合模式
*/
private void testComposite() {
TextFile textFileA = new TextFile("a.txt");
TextFile textFileB = new TextFile("b.txt");
TextFile textFileC = new TextFile("c.txt");
textFileA.watch();
// textFileA.add(textFileB);//调用会抛我们在抽象接口中写的异常
Folder folder = new Folder("学习资料");
folder.add(textFileA);
folder.add(textFileB);
folder.add(textFileC);
folder.watch();
folder.getChild(1).watch();
}
这里有一点是我们的抽象构件中包含组合方法和操作方法,有一些操作方法是树叶构件和树枝构件都会实现的,而组合方法是需要树枝构件来实现的,即我们对树叶构件的增删等操作。
Android源码中的组合模式
接下来我们思考一下Android源码中组合模式的应用,组合模式的使用场景是:表示一个对象整体-部分层次结构,即树形结构。而Android中典型的树形结构不就是我们的View体系么。在前边的模板方法模式中我们已经画过view的结构了,这里就不再画了,我们来一起分析一下:
Android源码中View是一个类(不是抽象类,不是接口),即我们的抽象构件。ViewGroup是View的子类,是一个抽象类,作为所有View的容器,即我们的树枝构件。而具体到特定的控件,比如Button、TextView等是我们的树叶构件。
通过观察我们的View类中是没有封装添加删除View的操作的,那么它们是在哪里添加到ViewGroup中的呢?我们发现ViewGroup实现了一个叫ViewManager的接口:
/** Interface to let you add and remove child views to an Activity. To get an instance
* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
*/
public interface ViewManager
{
/**
* Assign the passed LayoutParams to the passed View and add the view to the window.
* Throws {@link android.view.WindowManager.BadTokenException} for certain programming * errors, such as adding a second view to a window without removing the first view. *
Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a * secondary {@link Display} and the specified display can't be found * (see {@link android.app.Presentation}). * @param view The view to be added to this window. * @param params The LayoutParams to assign to view. */ public void addView(View view, ViewGroup.LayoutParams params); public void updateViewLayout(View view, ViewGroup.LayoutParams params); public void removeView(View view); }
ViewGroup实现这个接口的方法,所有就有了管理View的能力。
那么Google的开发者为什么要这么设计Android的View体系呢?组合模式可以让高层模块忽略了层次的差异,方便对整个层次结构进行控制。比如我们要向ViewGoup添加一个TextView或者添加一个LinearLayout,对这个ViewGroup来说是一样的。
搞定,收工。