组合模式(composite)

一.什么是组合模式

组合模式将对象组合成树形结构以表示"部分-整体"的层次结构。它使得用户对单个对象和组合对象的使用具有一致性,增强了代码的灵活性和可维护性。这种模式特别适用于需要表示层次结构的数据模型,如文件系统、组织结构图等‌。

二.类图

部分:left,不可分割,原子性

整体:composite,由部分组成

存放它们的容器:component。包含部分和整体

component可以调用left和compsite实例。

三.实例

参考图解设计模式这本书

这里用到了向上转型这种方式。向上转型在这里的作用是:将子类对象(File/Directory)当作父类类型(Entry)来统一处理,从而让容器可以混合存储不同子类型,并在遍历时以统一的父类视角操作它们。类图如图3-1所示。

图3-1

这里模拟创建文件,打印出所有文件的路径以及对应的文件存储大小。例如图3-2这样的文件结构

图3-2

打印出的效果如图3-3所示

图3-3

具体实现代码如下:

1.Entry.java

java 复制代码
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatmentException{
	throw new FileTreatmentException();
}
public void printList() {
	printList("root");
}
protected abstract void printList(String prefix);
public String toString() {
	return getName()+"("+getSize()+")";
}
}

这里的printList有两个,这就是重载。重载的定义就是在同一个类中,方法名相同,参数列表不同(类型、个数、顺序),与返回类型无关(仅改变返回类型不能 构成重载,编译器会报错)。重载的出现,体现了Java的编译时多态(也叫静态多态、早绑定)。下面的Directory类解说里面会提到具体的用法。

2.Directory.java

java 复制代码
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
	this.name=name;
}
@Override
public String getName() {//获取名字
		return name;
}

@Override
public int getSize() {//获取大小
		int size=0;
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		size+=entry.getSize();
	}
	return size;
}

@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
	System.out.println(prefix+"/"+this);
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		entry.printList(prefix+"/"+name);
	}
	
}
public Entry add(Entry entry) {//增加条目
	directory.add(entry);
	return this;
	
}

}

在getSize方法里面创建了一个迭代器it,用来遍历文件夹下面的每一个文件/文件夹,计算存储大小之和。在printList方法里面用了同样的方式遍历,打印出文件的路径,这里的 printList(String) 方法重写了父类 Entry 中同名的有参方法(参数列表必须完全相同)。方法内首先打印当前节点的路径:prefix + "/" + this,其中 this 是调用该方法的对象引用。若未重写 toString(),默认打印 类名@哈希码;但这里继承了 Entry 的 toString() 方法,以 对象名+(大小) 的格式输出。然后通过 Iterator 遍历 directory 集合中的每个子节点(Entry 类型)。对每个子节点递归调用 printList(prefix + "/" + name),其中 name 是当前节点(this)的名称,用于拼接下一级路径。由于 Entry 中的 printList(String) 是抽象方法(或有默认实现),子类 Directory 必须(或可以)重写该方法。递归调用时会根据子节点的实际类型(File 或 Directory)多态地执行对应的 printList 逻辑,最终打印出完整的目录树结构。

重写强烈建议加 @override,存在于继承关系的子类中,方法名必须相同,参数列表也必须完全相同。返回类型 :必须相同,或是原返回类型的子类 (协变返回类型)。重写的出现体现了运行时多态(也叫动态多态、晚绑定)。

3.File.java

java 复制代码
package composite;
public class File extends Entry{
private String name;
private int size;
public File(String name,int size) {
	this.name=name;
	this.size=size;
}
public String getName() {
	return name;
}
public int getSize() {
	return size;
}
protected void printList(String prefix) {
	System.out.println(prefix+"/"+this);
}
}

4.FileTreatmentException.java

java 复制代码
public class FileTreatmentException extends RuntimeException {
    
    // 无参构造器:什么都不做,只是调用父类无参构造器
    public FileTreatmentException() {
        super();  // 隐式存在,调用 RuntimeException()
    }
    
    // 有参构造器:接收消息,传给父类存储
    public FileTreatmentException(String msg) {
        super(msg);  // 调用 RuntimeException(String msg)
    }
}

常见的异常介绍

这个 FileTreatmentException 是一个运行时异常 ,在这里,当你对叶子节点(File)执行了只允许容器节点(Directory)执行的操作时抛出。

抛出异常测试代码

反射介绍

java 复制代码
FileTreatmentException e = new FileTreatmentException("文件操作非法");

// 这些都是从父类继承来的方法,你没有写任何代码
System.out.println(e.getMessage());        // 输出:文件操作非法
e.printStackTrace();                       // 打印完整堆栈
System.out.println(e.toString());          // 输出:composite.FileTreatmentException: 文件操作非法

// 查看继承的方法
Method[] methods = e.getClass().getMethods();
for (Method m : methods) {
    System.out.println(m.getName());
}
// 会看到 getMessage, printStackTrace, getCause... 等20+个方法

这里的e.getClass就是反射的应用。getClass()java.lang.Object 类中的方法,它返回一个 Class<?> 对象 ,这个对象包含了该实例的运行时类的元数据信息。这属于Java反射机制 的核心组成部分。反射就是在运行时动态地获取类的信息(如方法、字段、构造器)以及动态调用对象的能力。getClass() 就是反射的入口方法之一 。这里的getMethods() → 反射获取该类的所有 public 方法(包括从父类继承的)

5.Main.java

java 复制代码
package composite;
public class Main{
	public static void main(String []args) {
		try {
			Entry f1=new File("f1",10);
			Directory d1=new Directory("d1");
			Directory d2=new Directory("d2");
			d1.add(d2);
			File f2=new File("f2",20);
			d2.add(f2);
			f1.printList();
			d1.printList();
			System.out.println("**************************\n");
			d1.printList("");
					}
	catch(FileTreatmentException e){
		e.printStackTrace();
	}
}
}

这里打印有两种方式,printList()和printList(string)这两种方式

如果你使用无参函数,调用流程如下:

java 复制代码
// 第1步:你调用
d1.printList();  // 无参

// 第2步:进入 Entry 的无参方法
public void printList() {
    printList(" ");  // 内部主动调用有参版本,传入空格
}

// 第3步:进入有参方法,开始递归
protected void printList(String prefix) {
    System.out.println(prefix + "/" + this);
    // ... 递归调用子节点的有参方法
}

要是你使用带参数的函数,就会少了上面代码块的的第一步。

为什么父类要设计两个方法?

这是经典的模板方法模式

java 复制代码
// 对外公共API(用户友好,不需要传参)
public void printList() {
    printList("");  // 提供默认的初始前缀
}

// 内部递归实现(需要参数,但用户不需要知道)
protected void printList(String prefix) {
    // 递归逻辑
}

运行结果如下图所示:

四.习题

习题11-1:

树形结构,有递归的地方都适用组合模式,例如个体-班级-学校,页面组件-页面-页面容器。

习题11-12:

目前上面的printList只是查看节点之后是否还有文件或文件夹,如果有就打印出来。所以起点就是该节点,该节点之前的文件不清楚,我们要打印文件的路径,需要知道该节点之前的文件。思路是在每次加一个文件夹的时候,前缀就变成该节点名称+"/",那怎么实现呢?具体是添加一个parent父节点作为零时存储节点,每次add的时候就把"前缀+该节点的文件名"作为新的前缀存储进parent节点的name里面。也就是parent的文件名就是前缀。

实现代码如下:

1.Entry.java

首先加一个entry对象parent,再编写一个getFullName方法,在getFullName方法里面,定义一个StringBuffer对象,将调用getFullName方法的节点存储在entry里面,通过循环向前寻找父节点,不断增加前缀,直到前面没有节点位置。此时就是完整的路径,最后通过fullname.tostring方法返回字符串。

java 复制代码
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry parent;
public Entry add(Entry entry) throws FileTreatmentException{
	throw new FileTreatmentException();
}
public void printList() {
	printList("root");
}
protected abstract void printList(String prefix);
public String toString() {
	return getName()+"("+getSize()+")";
}
public String getFullName() {
	StringBuffer fullname=new StringBuffer();
	Entry entry=this;
	do {
		fullname.insert(0, "/"+entry.getName());
		entry=entry.parent;
	}while(entry!=null);
	return fullname.toString();
}
}

2.Directory.java

文件夹才可以add文件或文件夹,因此,add方法写在文件夹类里面,但是为了表示文件不能使用add方法,所以在entry里面添加add方法用来抛出异常,这样用户在使用文件add或是entry来add的时候会抛出异常,就方便理解。在add的时候就要把该节点存储在parent里面,这样每次add的时候,parent的文件名(即前缀)就会改变。

java 复制代码
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
	this.name=name;
}
@Override
public String getName() {//获取名字
		return name;
}

@Override
public int getSize() {//获取大小
		int size=0;
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		size+=entry.getSize();
	}
	return size;
}

@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
	System.out.println(prefix+"/"+this);
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		entry.printList(prefix+"/"+name);
	}
}
public Entry add(Entry entry) {//增加条目
	directory.add(entry);
	entry.parent=this;
	return this;
	
}


}

我实现了打印一个文件夹下的所有文件功能getFiles函数。仿照printList写的,供大家了解一下。

1.entry.java

java 复制代码
package composite;
import java.util.*;
public abstract class Entry {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatmentException{
	throw new FileTreatmentException();
}
public void printList() {
	printList("root");
}
protected abstract void printList(String prefix);
protected abstract String getFiles(String path);
public void getFiles() {
	String path="";
	path=getFiles(path);//如果一个无返回值函数(void)调用一个递归有返回值函数,必须要有变量接收返回值,否则返回值会丢失。
	printPath(path);
}
public void printPath(String path) {
	System.out.println(path);
}
public String toString() {
	return getName()+"("+getSize()+")";
}
}

2.Directory.java

java 复制代码
package composite;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry{
private String name;
private ArrayList directory=new ArrayList();
public Directory(String name) {
	this.name=name;
}
@Override
public String getName() {//获取名字
		return name;
}

@Override
public int getSize() {//获取大小
		int size=0;
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		size+=entry.getSize();
	}
	return size;
}

@Override
protected void printList(String prefix) {//显示条目目录一览prefix前缀
	System.out.println(prefix+"/"+this);
	Iterator it=directory.iterator();
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		entry.printList(prefix+"/"+name);
	}
}
public Entry add(Entry entry) {//增加条目
	directory.add(entry);
	return this;
	
}
@Override
protected String getFiles(String path) {
	// TODO Auto-generated method stub
	Iterator it=directory.iterator();
	String currentPath=path+"/"+this;
	while(it.hasNext()) {
		Entry entry=(Entry)it.next();
		currentPath=entry.getFiles(currentPath);
	}
	return currentPath;
}

}

3.File.java

java 复制代码
package composite;
public class File extends Entry{
private String name;
private int size;
public File(String name,int size) {
	this.name=name;
	this.size=size;
}
public String getName() {
	return name;
}
public int getSize() {
	return size;
}
protected void printList(String prefix) {
	System.out.println(prefix+"/"+this);
}
@Override
protected String getFiles(String path) {
	// TODO Auto-generated method stub
	return path+"/"+this;	
	
}
}

4.Main.java

java 复制代码
package composite;
public class Main{
	public static void main(String []args) {
		try {
			Entry f1=new File("f1",10);
			Directory d1=new Directory("d1");
			Directory d2=new Directory("d2");
			d1.add(d2);
			File f2=new File("f2",20);
			File f3=new File("f3",30);
			d2.add(f2);
			d2.add(f3);
			d2.getFiles();
			f1.getFiles();
			
					}
	catch(FileTreatmentException e){
		e.printStackTrace();
	}
}
}
相关推荐
石逸凡13 天前
论组织本源与钻形式招牌的空子
大数据·组合模式
雪碧聊技术14 天前
什么是组合模式?一文详解
组合模式
c++之路18 天前
组合模式(Composite Pattern)
组合模式
likerhood22 天前
设计模式 · 组合模式(Composite Pattern)
设计模式·组合模式
蜡笔小马24 天前
07.C++设计模式-组合模式
c++·设计模式·组合模式
多加点辣也没关系25 天前
设计模式-组合模式
设计模式·组合模式
qq_296553271 个月前
[特殊字符] 数组中的递增三元组:O(n) 时间高效查找,面试必考!
数据结构·算法·面试·职场和发展·组合模式·柔性数组
geovindu1 个月前
go: Composite Pattern
设计模式·golang·组合模式
ximu_polaris1 个月前
设计模式(C++)-结构型模式-组合模式
c++·设计模式·组合模式