这篇我们来介绍一下组合模式(Composite Pattern),它也称为部分整体模式(Part-Whole Pattern),结构型模式之一。组合模式比较简单,它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象集合之间的差别。这个最典型的例子就是数据结构中的树了,如果一个节点有子节点,那么它就是枝干节点,如果没有子节点,那么它就是叶子节点,那么怎么把枝干节点和叶子节点统一当作一种对象处理呢?这就需要用到组合模式了。
转载请注明出处:http://blog.csdn.net/self_study/article/details/51761709。
PS:对技术感兴趣的同鞋加群544645972一起交流。
设计模式总目录
特点
组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构,并且能够让客户端以一致的方式处理个别对象以及组合对象。
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合构件以及叶子构件的对象,而且能够把相同的操作应用在组合构件和叶子构件上,换句话说,在大多数情况下我们可以忽略组合对象和叶子对象之间的差别。组合模式使用的场景:
- 表示对象的部分-整体结构层次时;
- 从一个整体中能够独立出部分模块或功能的场景。
UML类图
组合模式在实际使用中会有两种情况:安全的组合模式与透明的组合模式。
安全的组合模式
我们先来看看安全组合模式的 uml 类图:
可以看到组合模式有 4 个角色:
- Component:抽象根节点,为组合中的对象声明接口行为,是所有节点的抽象。在适当的情况下,实现所有类共有接口的缺省行为。声明一个接口用于访问和管理 Component 的子节点。可在递归结构中定义一个接口,用于访问一个父节点,并在合适的情况下实现它;
- Composite:增加定义枝干节点的行为,存储子节点,实现 Component 接口中的有关的操作;
- Leaf:在组合中表示叶子结点对象,叶子节点没有子节点,实现 Component 接口中的全部操作;
- Client:通过 Component,Composite 和 Leaf 类操纵组合节点对象。
据此我们可以写出安全组合模式的通用代码:
Component.class
public abstract class Component {
public abstract void operation();
}
Composite.class
public class Composite extends Component{
private ArrayList<Component> componentList = new ArrayList<>();
@Override
public void operation() {
Log.e("shawn", "this is composite " + this + " -------start");
for (Component component : componentList) {
component.operation();
}
Log.e("shawn", "this is composite " + this + " -------end");
}
public void add(Component child) {
componentList.add(child);
}
public void remove(Component child) {
componentList.remove(child);
}
public Component getChild(int position) {
return componentList.get(position);
}
}
Leaf.class
public class Leaf extends Component{
@Override
public void operation() {
Log.e("shawn", "this if leaf " + this);
}
}
Client 测试代码:
Composite root = new Composite();
Leaf leaf1 = new Leaf();
Composite branch = new Composite();
root.add(leaf1);
root.add(branch);
Leaf leaf2 = new Leaf();
branch.add(leaf2);
root.operation();
break;
最后输出结果:
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@a37f4d8 -------start
com.android.compositepattern E/shawn: this if leaf com.android.compositepattern.composite.Leaf@1d7d4031
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@ec97316 -------start
com.android.compositepattern E/shawn: this if leaf com.android.compositepattern.composite.Leaf@5dae497
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@ec97316 -------end
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@a37f4d8 -------end
代码很简单,结果就是一个简单的树形结构,但是仔细看看客户端代码,就能发现它违反了 6 个设计模式原则中依赖倒置原则,客户端不应该直接依赖于具体实现,而应该依赖于抽象,既然是面向接口编程,就应该把更多的焦点放在接口的设计上,于是这样就产生了透明的组合模式。
透明的组合模式
来看看透明的组合模式 uml 类图:
和安全的组合模式差异就是在将 Composite 的操作放到了 Component 中,这就造成 Leaf 角色也要实现 Component 中的所有方法。实现的代码做出相应改变:
Component.class
public interface Component {
void operation();
void add(Component child);
void remove(Component child);
Component getChild(int position);
}
Composite.class
public class Composite implements Component{
private ArrayList<Component> componentList = new ArrayList<>();
@Override
public void operation() {
Log.e("shawn", "this is composite " + this + " -------start");
for (Component component : componentList) {
component.operation();
}
Log.e("shawn", "this is composite " + this + " -------end");
}
@Override
public void add(Component child) {
componentList.add(child);
}
@Override
public void remove(Component child) {
componentList.remove(child);
}
@Override
public Component getChild(int position) {
return componentList.get(position);
}
}
Leaf.class
public class Leaf implements Component {
@Override
public void operation() {
Log.e("shawn", "this if leaf " + this);
}
@Override
public void add(Component child) {
throw new UnsupportedOperationException("leaf can‘t add child");
}
@Override
public void remove(Component child) {
throw new UnsupportedOperationException("leaf can‘t remove child");
}
@Override
public Component getChild(int position) {
throw new UnsupportedOperationException("leaf doesn‘t have any child");
}
}
Client 测试代码
Component root = new Composite();
Component leaf1 = new Leaf();
Component branch = new Composite();
root.add(leaf1);
root.add(branch);
Component leaf2 = new Leaf();
branch.add(leaf2);
root.operation();
最后产生的结果是一样的,由于是在 Component 类中定义了所有的行为,所以客户端就不用直接依赖于具体 Composite 和 Leaf 类的实现,遵循了依赖倒置原则——依赖抽象,而不依赖具体实现。但是也违反了单一职责原则与接口隔离原则,让 Leaf 类继承了它本不应该有的方法,并且不太优雅的抛出了 UnsupportedOperationException ,这样做的目的就是为了客户端可以透明的去调用对应组件的方法,将枝干节点和子节点一视同仁。
另外,将 Component 写成一个虚基类,并且实现所有的 Composite 方法,而且默认都抛出异常,只让 Composite 去覆盖重写父类的方法,而 Leaf 类就不需要去实现 Composite 的相关方法,这么去实现当然也是可以的。
对比
安全的组合模式将责任区分开来放在不同的接口中,这样一来,设计上就比较安全,也遵循了单一职责原则和接口隔离原则,但是也让客户端必须依赖于具体的实现;透明的组合模式,以单一职责原则和接口隔离原则原则换取透明性,遵循依赖倒置原则,客户端就直接依赖于 Component 抽象即可,将 Composite 和 Leaf 一视同仁,也就是说,一个元素究竟是枝干节点还是叶子节点,对客户端是透明的。
所以这是一个很典型的折衷案例,尽管我们受到设计原则的指导,但是我们总是需要观察某原则对我们的设计所造成的影响。有时候这个需要去根据实际案例去分析,毕竟有些时候 6 种设计模式原则在实际使用过程中是会冲突的,是让客户端每次使用的时候都去先检查类型还是赋予子节点不应该有的行为,这都取决于设计者的观点,总体而言,这两种方案都是可行的。
示例与源码
组合模式在实际生活过程中的例子就数不胜数了,比如菜单、文件夹等等。我们这就以 Android 中非常经典的实现为例来分析一下。View 和 ViewGroup 想必应该都非常熟悉,其实他们用到的就是组合模式,我们先来看看他们之间的 uml 类图:
ViewManager 这个类在java/android 设计模式学习笔记(8)—桥接模式中提到过,WindowManager 也继承了该类:
/** 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.
* <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
* errors, such as adding a second view to a window without removing the first view.
* <p>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);
}
只定义了关于 View 操作的三个方法。ViewParent 类是用来定义一个 父 View 角色所具有的职责,在 Android 中,一般能成为父 View 的也只有 ViewGroup:
/**
* Defines the responsibilities for a class that will be a parent of a View.
* This is the API that a view sees when it wants to interact with its parent.
*
*/
public interface ViewParent {
/**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/
public void requestLayout();
/**
* Indicates whether layout was requested on this view parent.
*
* @return true if layout was requested, false otherwise
*/
public boolean isLayoutRequested();
....
}
从 uml 类图中可以注意到一点,ViewGroup 和 View 使用的安全的组合模式,而不是透明的组合模式,怪不得有时候使用前需要将 View 强转成 ViewGroup 。
总结
使用组合模式,我们能把相同的操作应用在组合和个别对象上,换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。组合模式适用于一些界面 UI 的结构设计上,典型的例子就是Android,iOS 和 Java 等都提供了相应的 UI 框架。
组合模式的优点:
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让高层模块忽略了层次的差异,方便对整个层次结构进行控制;
- 高层模块可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是组合结构,简化了高层模块的代码。
- 在组合模式中增加新的枝干构件和叶子构件都很方便,无需对现有类库进行任何修改,符合“开闭原则”;
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和枝干对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
组合模式的缺点:在新增构件时不好对枝干中的构建类型进行限制,不能依赖类型系统来施加这些约束,因为在大多数情况下,他们都来自于相同的抽象层,此时,必须进行类型检查来实现,这个实现过程较为复杂。
源码下载
https://github.com/zhaozepeng/Design-Patterns/tree/master/CompositePattern
引用
https://en.wikipedia.org/wiki/Composite_pattern