Java设计模式-组合模式

组合(Composite)模式,在23种设计模式中属于对象结构型模式。

组合模式将对象整理成树形结构以表示“部分-整体”的层次结构,它可以使用户对单个对象和组合对象的操作具有一致性。

模式介绍

从接触面向对象语言起,我们就被告知封装、继承和多态是面向对象的三大特性,其中继承和多态中的重写特性更是使用继承机制的直观体现。随着时间的推移,我们对面向对象语言的经验理解也不断增强,在各种编程实践书籍以及设计模式中开始接触更多的建议:少用继承,多用组合。

在23种设计模式出现之初,就把组合模式作为其中一种模式而罗列其中了,足见组合在编程中重要性。平常使用的软件中,如操作系统目录结构,界面上用于分类的目录树,甚至一二或者多级评论都有组合模式的应用。良好的类结构设计可以给后续编码带来极大的便利,如何设计出灵活性高且易于扩展的类正是组合模式探讨研究的课题。

适用性

  • 表示具有部分-整理的层次结构。
  • 忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象。

透明组合模式示例

// 抽象组件类
public abstract class File {
	private String name;
	public File(String name) {
		this.name = name;
	}
	
	// 省略了setter getter方法
	
	public abstract void add(File file);

	public abstract void remove(File file);

	public abstract void printTreeTag(String prefix);
}
// 容器组件类
public class Folder extends File {
	private List files = new ArrayList<>();

	public Folder(String name) {
		super(name);
	}

	public void add(File file) {
		files.add(file);
	}

	public void remove(File file) {
		files.remove(file);
	}

	@Override
	public void printTreeTag(String prefix) {
		prefix = prefix == "" ? getName() : prefix + "/" + getName();
		System.out.println(prefix);
		for (File file : files) {
			file.printTreeTag(prefix);
		}
	}
}
// 叶子节点
public class TextFile extends File {
	public TextFile(String name) {
		super(name);
	}
	@Override
	public void add(File file) {
		throw new UnsupportedOperationException();
	}
	@Override
	public void remove(File file) {
		throw new UnsupportedOperationException();
	}
	@Override
	public void printTreeTag(String prefix) {
		System.out.println(prefix + "/" + getName());
	}
}
// 测试类
public class Client {
	public static void main(String[] args) {
		File file=new Folder("C:/");
		
		File mediaFolder=new Folder("多媒体文件夹");
		File videoFolder=new Folder("视频文件夹");
		File videoFile01=new VideoFile("复仇者联盟四.avi");
		File videoFile02=new VideoFile("正义联盟.mkv");
		File videoFile03=new VideoFile("疯狂动物城.rmvb");
		File textFile=new TextFile("Java设计模式.java");
		File imageFile=new ImageFile("composite_pattern.png");
		
		videoFolder.add(videoFile01);
		videoFolder.add(videoFile02);
		videoFolder.add(videoFile03);
		mediaFolder.add(videoFolder);
		
		file.add(mediaFolder);
		file.add(textFile);
		file.add(imageFile);
		
		file.printTreeTag("");
	}
}

代码执行结果如下:

C:/
C://多媒体文件夹
C://多媒体文件夹/视频文件夹
C://多媒体文件夹/视频文件夹/复仇者联盟四.avi
C://多媒体文件夹/视频文件夹/正义联盟.mkv
C://多媒体文件夹/视频文件夹/疯狂动物城.rmvb
C://Java设计模式.java
C://composite_pattern.png

安全组合模式示例

// 抽象组件类
public abstract class MenuComponent {
	private int id;
	private String name;
	
	public MenuComponent(int id, String name) {
		this.id = id;
		this.name = name;
	}

	// 省略了setter getter方法
	
	public abstract void print();
}
// 容器组件
public class Menu extends MenuComponent{
	
	List menuComponents=new ArrayList<>();

	public Menu(int id, String name) {
		super(id, name);
	}
	public void add(MenuComponent menuComponent){
		menuComponents.add(menuComponent);
	}
	public void remove(MenuComponent menuComponent){
		menuComponents.remove(menuComponent);
	}
	@Override
	public void print() {
		System.out.println("Menu [id=" + getId() + ", name=" + getName() + "]");
		for(MenuComponent menu:menuComponents){
			System.out.print("|--- ");
			menu.print();
		}
	}
}
// 叶子节点
public class MenuItem extends MenuComponent {

	private String icon;

	public MenuItem(int id, String name, String icon) {
		super(id, name);
		this.icon = icon;
	}
	
	// 省略了setter getter方法

	@Override
	public void print() {
		System.out.println("Item [id=" + getId() + ", name=" + getName() + ", icon=" + icon + "]");
	}
}
public class Client {
	public static void main(String[] args) {
		Menu menu01=new Menu(1001, "Menu_A01");
		menu01.add(new MenuItem(10001, "Item_A01", "Icon_A01"));
		menu01.add(new MenuItem(10002, "Item_A02", "Icon_A02"));
		menu01.add(new MenuItem(10003, "Item_A03", "Icon_A03"));
		menu01.print();
		
		Menu menu02=new Menu(2001, "Menu_B01");
		menu02.add(new MenuItem(20001, "Item_B01", "Icon_B01"));
		menu02.add(new MenuItem(20002, "Item_B02", "Icon_B02"));
		menu02.print();	
	}
}

代码执行结果如下:

Menu [id=1001, name=Menu_A01]
|--- Item [id=10001, name=Item_A01, icon=Icon_A01]
|--- Item [id=10002, name=Item_A02, icon=Icon_A02]
|--- Item [id=10003, name=Item_A03, icon=Icon_A03]
Menu [id=2001, name=Menu_B01]
|--- Item [id=20001, name=Item_B01, icon=Icon_B01]
|--- Item [id=20002, name=Item_B02, icon=Icon_B02]

模式分析

组合模式类图如下:

  • Component抽象组件:可以使接口或者抽象类,为叶子节点或者容器声明接口,在适当情况下可以定义一些接口的缺省行为,声明接口用于访问和管理子组件。
  • Leaf叶子组件:在组合中表示叶子节点对象,也可以说表示的使一个基本对象,对于Component声明的访问和管理子组件的方法不实现或者直接抛出异常处理即可。
  • Composite容器组件:用于存储子组件,实现在Component中定义的行为,包括那些访问及管理子组件的方法,在其业务方法中可以递归调用其子组件的业务方法。

组合模式的关键是定义了一个抽象组件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象组件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。

组合模式根据抽象组件Component的定义形式,可以分为透明组合模式安全组合模式

透明组合模式中抽象组件Component定义了管理成员对象的方法,如add()、remove()以及getChild()等方法,这样做的好处是所有的组件都具有相同的接口类型。虽然透明组合模式是组合模式的标准形式,但是这样做是以安全性为代价的。最后层级的叶子对象不可能有下一层级对象,但是为其提供了add()、remove()以及getChild()等方法,无法确保客户端在使用该对象时不会调用这些无意义的方法。

安全组合模式是将管理成员的方法都定义在了Composite容器组件中,这种做法是相对安全的,因为叶子节点本来就不需要这些方法。但是安全组合模式又不够透明,因为叶子组件和容器组件具有不同的方法,并且容器中的用于管理成员对象的方法没有定义在抽象组件中,因此在使用时这些对象时不能完全根据抽象编程,必须区分叶子组件和容器组件。在实际应用时安全组件的使用频率也非常的高,Java AWT中使用的组合模式就是安全组合模式。

安全组合模式和透明组合模式需要在平常使用中根据业务员场景做平衡,一种方式就是在Component抽象组件中定义一个用于返回Composite容器组件的方法,类似getComposite()这种方法。

模式优点

组合模式清晰地定义基本对象和组合对象的类层次结构,基本对象可以组合成更复杂的组合对象,而这个组合对象又可以被重新组合,这样不断递归下去。客户端任何用到基本对象的地方都可以用到组合对象。

简化了客户代码,客户可以一致地使用组合对象和基本对象。

在组合模式中增加新的组合对象和基本对象都很方便,无须对现有类库进行任何修改。

模式缺点

组合模式可以一致地操作基本对象和组合对象,不过这也会产生一些问题,如果希望组合对象中只能包含某些特定对象,这时候不能依赖系统类型进行约束,必须运行时进行类型检查。

与其它模式关系

组合模式和装饰模式都是对象结构型模式,而且两个结构也有很大的相识之处,但是两者的出发点却有根本性的区别,组合模式在于可以使基本对象和组合对象使用上具有一致性,用于组建部分-整体的对象结构。而装饰模式在于将不是一个类型的对象包装成相同类型的对象,它的目的不在于聚合。

组合对象有时会借助迭代器模式用于遍历其中的子对象。

小结

组合模式主要是为了让我们把相同的操作应用在组合对象和单个对象上面,全局结构上考虑的是为了表示“部分-整体”关系。换句话说,在组合结构下我们基本可以忽略组合对象和单个对象之间的差异。

组合模式在实际应用中也非常广泛,Java SE中的AWT和Swing包的设计就基于组合模式,在上文菜单示例就是仿写的AWT菜单。良好的类设计,可以给开发带来很大的遍历,所以组合模式并没有明确表示在开发中你需要使用安全组合模式还是透明组合模式,这完全取决于开发中遇到的具体场景。

在某一期的乔布斯访谈中有这样一段描述:

生活中多数东西,最好与普通之间的差异不超过两倍。好比说纽约的出租车司机,最棒的司机与普通司机之间的差距大概是30%,最好与普通之间的差距有多大呢?可能一般20%吧...但是在软件行业,还有硬件行业,这种差距有可能超过50倍,甚至100倍。

学习设计模式并不会让你一跃成为金字塔顶端的开发者,但是确实为你软件开发道路指明了方向。三体语录:弱小和无知不是生存的障碍,傲慢才是。为什么学习设计模式?很简单的回答,已经有人为你指明了使用“这种”模式可以解决“这种”类型的问题,为什么不学习了解一下呢。

示例源代码下载,提取码:j9cm

评论

您确定要删除吗?删除之后不可恢复