设计模式之组合模式(Composite)
1.引言
在学习JUnit的时候,看到一段话“JUnit框架是一个典型的Composite模式:TestSuite可以容纳任何派生自Test的对象;当调用TestSuite对象的run()方法是,会遍历自己容纳的对象,逐个调用它们的run()方法”。就来学习什么是组合模式。
2.应用实例
在实现跟商品有关的应用系统的时候,一个很常见的功能就是商品类别树的管理,比如有以下的商品类别树:
——————————————————————————————————
-服装
-男装
-衬衣
-夹克
-女装
-裙子
-套装
——————————————————————————————————
通过上面的商品类别树我们可以发现商品类别树有两种类型的节点,分别是叶子节点(衬衣,夹克)和组合节点(服装,男装,女装)。组合节点中可以包含其他的组合节点或者叶子节点,而叶子节点不能。
给出一个简单的管理商品类别树的实例代码。
2.1不使用组合模式的解决方案
组合节点Composite
package edu.sjtu.erplab.designpattern.composite.exp1;
import java.util.ArrayList;
import java.util.Collection;
public class Composite {
private Collection<Composite> childComposite=new ArrayList<Composite>();
private Collection<Leaf> childLeaf=new ArrayList<Leaf>();
private String name="";
public Composite(String name)
{
this.name=name;
}
//向组合对象中加入其他组合对象
public void addComposite(Composite c)
{
this.childComposite.add(c);
}
//向组合对象加入其他叶子对象
public void addLeaf(Leaf l)
{
this.childLeaf.add(l);
}
//输出组合对象结构
public void printStruct(String preStr)
{
System.out.println(preStr+"-"+name);
preStr+=" ";
for(Leaf leaf:childLeaf)
{
leaf.printStruct(preStr);
}
for(Composite composite:childComposite)
{
composite.printStruct(preStr);
}
}
}
叶子节点
package edu.sjtu.erplab.designpattern.composite.exp1;
public class Leaf {
private String name="";
public Leaf(String name){
this.name=name;
}
public void printStruct(String preStr)
{
System.out.println(preStr+"-"+name);
}
}
客户端Client
package edu.sjtu.erplab.designpattern.composite.exp1;
public class Client {
public static void main(String args[])
{
//定义组合对象
Composite root=new Composite("服装");
Composite c1=new Composite("男装");
Composite c2=new Composite("女装");
//定义叶子对象
Leaf l1=new Leaf("衬衣");
Leaf l2=new Leaf("夹克");
Leaf l3=new Leaf("裙子");
Leaf l4=new Leaf("套装");
//按照树的结构来组合对象
root.addComposite(c1);
root.addComposite(c2);
c1.addLeaf(l1);
c1.addLeaf(l2);
c2.addLeaf(l3);
c2.addLeaf(l4);
root.printStruct("");
}
}
运行结果就是应用实例中显示的商品树形结构。
上述解决方案存在的问题:
虽然实现了要求的功能,但是有一个明显的问题:那就是必须区分组合对象和叶子对象,并进行区别对待,比如在Composite(addComposite方法和addLeaf方法)和Client(定义Composite对象和定义Leaf对象)里面,都需要去区别对待这两种对象。区别对待组合对象与叶子对象不但让程序更加复杂,还对功能的扩展带了了不便。用户不希望区别对待这两类对象。
2.2使用组合模式的解决方案
组合模式通过引入一个抽象的组件对象,作为组合对象和叶子对象的父对象,这样就把组合对象和叶子对象统一起来了,用户使用的时候,始终是在操作组件对象,而不再去区分是在操作组合对象还是叶子对象。
组合模式的关键就在于这个抽象类,这个抽象类既可以代表叶子对象,也可以代表组合对象,这样用户在操作的时候,对叶子对象和组合对象的使用就具有了一致性。
组件对象Component
package edu.sjtu.erplab.designpattern.composite.exp2;
public abstract class Component {
public abstract void printStruct(String preStr);
//透明性
public void addChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
public void removeChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
public Component getChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
}
组合对象Composite
package edu.sjtu.erplab.designpattern.composite.exp2;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Composite extends Component {
private List<Component> childComponents = null;
private String name = "";
public Composite(String name)
{
this.name=name;
}
@Override
public void addChild(Component child) {
// TODO Auto-generated method stub
if (childComponents == null) {
childComponents = new ArrayList<Component>();
}
childComponents.add(child);
}
@Override
public void removeChild(Component child) {
// TODO Auto-generated method stub
super.removeChild(child);
}
@Override
public Component getChild(Component child) {
// TODO Auto-generated method stub
return super.getChild(child);
}
public void printStruct(String preStr) {
System.out.println(preStr + "-" + name);
if (this.childComponents != null) {
preStr += " ";
for (Component c : childComponents) {
c.printStruct(preStr);
}
}
}
}
叶子对象Leaf
package edu.sjtu.erplab.designpattern.composite.exp2;
public class Leaf extends Component {
private String name="";
public Leaf(String name){
this.name=name;
}
//必须实现父类的抽象方法
public void printStruct(String preStr)
{
System.out.println(preStr+"-"+name);
}
}
客户端
package edu.sjtu.erplab.designpattern.composite.exp2;
public class Client {
public static void main(String args[])
{
//定义组合对象
Component root=new Composite("服装");
Component c1=new Composite("男装");
Component c2=new Composite("女装");
//定义叶子对象
Component l1=new Leaf("衬衣");
Component l2=new Leaf("夹克");
Component l3=new Leaf("裙子");
Component l4=new Leaf("套装");
//按照树的结构来组合对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(l1);
c1.addChild(l2);
c2.addChild(l3);
c2.addChild(l4);
root.printStruct("");
}
}
程序架构如下图所示。这样的架构实现了用户的透明访问。
3.透明性与安全性的权衡考虑
如上图所示,在Component组件中定义了操作组合节点的方法addChild,removeChild等,这些方法被Leaf继承,因此Leaf也能够调用,但是叶子节点是不能进行增加子节点和删除子节点的。这样就存在安全性的问题。
基于安全性考虑的组合模式的解决方案
组件节点Component
package edu.sjtu.erplab.designpattern.composite.exp3;
public abstract class Component {
public abstract void printStruct(String preStr);
}
组合节点Composite和叶子节点Leaf没有改变
客户端发生改变,需要区分组合节点和叶子节点,这个跟第一个代码实例中类似。
package edu.sjtu.erplab.designpattern.composite.exp3;
public class Client {
public static void main(String args[])
{
//定义组合对象
Composite root=new Composite("服装");
Composite c1=new Composite("男装");
Composite c2=new Composite("女装");
//定义叶子对象
Leaf l1=new Leaf("衬衣");
Leaf l2=new Leaf("夹克");
Leaf l3=new Leaf("裙子");
Leaf l4=new Leaf("套装");
//按照树的结构来组合对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(l1);
c1.addChild(l2);
c2.addChild(l3);
c2.addChild(l4);
root.printStruct("");
}
}
上述实例的结构如下图所示:
透明性组合模式与安全性组合模式的选择:
对于组合模式,在安全性和透明性上,会更加看重透明性,毕竟组合模式的功能就是让客户端对叶子对象和组合对象的使用具有一致性。
4.父组件引用
在上面的示例中,都是在父组件对象中,保存有子组件的引用,也就是说都是从父到子的引用,本节讨论一下子组件对象到父组件对象的引用,在实际开发过程中也非常有用,比如可以实现如下功能:
- 删除某个商品的类别, 如果是子对象,那么直接删除,如果是组合对象,那么将组合对象下的所有子对象的层级提高一层。
- 调整商品类别。
要实现上述功能, 一个较为简单的方案就是在保持从父组件到子组件引用的基础上,再增加保持从子组件到父组件的引用,这样在删除一个组件对象或者是调整一个组件对象的时候,可以通过调整父组件的引用来实现,可以大大简化实现。
实例说明如下:
组件对象Component
package edu.sjtu.erplab.designpattern.composite.exp4;
import java.util.List;
public abstract class Component {
private Component parent=null;
//获取某个组件的所有子对象
public List<Component> getChildren()
{
throw new UnsupportedOperationException("对象不支持这个功能");
}
//获取父对象
public Component getParent() {
return parent;
}
public void setParent(Component parent) {
this.parent = parent;
}
public abstract void printStruct(String preStr);
public void addChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
public void removeChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
public Component getChild(Component child){
throw new UnsupportedOperationException("对象不支持这个功能");
}
}
组合对象Composite
package edu.sjtu.erplab.designpattern.composite.exp4;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Composite extends Component {
private List<Component> childComponents = null;
private String name = "";
public Composite(String name)
{
this.name=name;
}
public void printStruct(String preStr) {
System.out.println(preStr + "-" + name);
if (this.childComponents != null) {
preStr += " ";
for (Component c : childComponents) {
c.printStruct(preStr);
}
}
}
/**
* 添加子组件
* child为具体的子组件
* 为child设置父组件。setParent
*/
@Override
public void addChild(Component child) {
if (childComponents == null) {
childComponents = new ArrayList<Component>();
}
childComponents.add(child);
//添加对父组件的引用
child.setParent(this);
}
/**
* 删除子组件
*/
@Override
public void removeChild(Component child) {
if(childComponents!=null)
{
int idx=childComponents.indexOf(child);
if(idx!=-1)
{
for(Component c:child.getChildren())//获取被删除组件的所有子组件
{
c.setParent(this);//设置父类别
childComponents.add(c);//更改结构,添加子类别
}
childComponents.remove(idx);
}
}
}
@Override
public List<Component> getChildren()
{
return childComponents;
}
}
叶子节点没有变化,客户端有所改变
package edu.sjtu.erplab.designpattern.composite.exp4;
public class Client {
public static void main(String args[])
{
//定义组合对象
Component root=new Composite("服装");
Component c1=new Composite("男装");
Component c2=new Composite("女装");
//定义叶子对象
Component l1=new Leaf("衬衣");
Component l2=new Leaf("夹克");
Component l3=new Leaf("裙子");
Component l4=new Leaf("套装");
//按照树的结构来组合对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(l1);
c1.addChild(l2);
c2.addChild(l3);
c2.addChild(l4);
root.printStruct("");
System.out.println("---------------------------------");
root.removeChild(c1);
root.printStruct("");
}
}
程序运行结果:
——————————————————————————
-服装
-男装
-衬衣
-夹克
-女装
-裙子
-套装
---------------------------------
-服装
-女装
-裙子
-套装
-衬衣
-夹克
——————————————————————
程序架构结构图如下图所示:
5组合模式的优缺点及适用环境
优点:
- 定义了包含基本对象和组合对象的层次结构
- 统一了组合对象和叶子对象
- 简化了客户端调用
- 更容易扩展
缺点:
- 很难闲置组合中的组件类型
适用环境
- 如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,是的层次结构实现更简单,从外部来使用这个层次结构也更加容易。
- 如果你希望统一得使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能。