一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
本文详细介绍了组合模式的设计思想和实现方法,涵盖组合模式的基础概念、实现步骤、实例演示、实现方式、优缺点分析等内容。通过具体的代码案例,展示了如何使用组合模式来处理具有层次结构的对象,如文件系统和购物清单,使客户端可以一致地处理单个对象和组合对象。文章还探讨了透明式和安全式组合模式的区别,并提供了设计建议和适用场景。适合初学者和有一定经验的开发者阅读。
客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
如何将“客户代码与复杂的对象容器结构”解耦?让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来处理复杂的对象容器?
定义:将对象以树形结构组织起来,以达成“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。
使用场景
当我们思考组合模式时,以下几个方面值得考虑:
主要解决的问题
假设我们有一个文件系统,其中有两种类型的文件:文本文件和文件夹。文本文件是叶子节点,文件夹是组合节点,可以包含其他文件。我们想要使用组合模式来实现文件系统的层次结构,并且提供一个打印文件路径的方法。
组合模式包含如下角色:更多内容
定义抽象组件(Component)
public interface File {
// 获取文件名称
String getName();
// 添加子文件
void add(File file);
// 删除子文件
void remove(File file);
// 获取子文件
List<File> getChildren();
// 打印文件路径
void printPath(int space);
}
定义叶子节点(Leaf)
public class TextFile implements File {
private String name;
public TextFile(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
throw new UnsupportedOperationException("Text file cannot add child file");
}
@Override
public void remove(File file) {
throw new UnsupportedOperationException("Text file cannot remove child file");
}
@Override
public List<File> getChildren() {
throw new UnsupportedOperationException("Text file has no child file");
}
@Override
public void printPath(int space) {
StringBuilder sp = new StringBuilder();
for (int i = 0; i < space; i++) {
sp.append(" ");
}
System.out.println(sp + name);
}
}
定义组合节点
public class Folder implements File {
private String name;
private List<File> children;
public Folder(String name) {
this.name = name;
children = new ArrayList<>();
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
children.add(file);
}
@Override
public void remove(File file) {
children.remove(file);
}
@Override
public List<File> getChildren() {
return children;
}
@Override
public void printPath(int space) {
StringBuilder sp = new StringBuilder();
for (int i = 0; i < space; i++) {
sp.append(" ");
}
System.out.println(sp + name);
space += 2;
for (File child : children) {
child.printPath(space);
}
}
}
客户端代码
private void test() {
// 创建一个根文件夹,并添加两个文本文件和一个子文件夹
File root = new Folder("root");
root.add(new TextFile("a.txt"));
root.add(new TextFile("b.txt"));
File subFolder = new Folder("subFolder");
root.add(subFolder);
// 在子文件夹中添加两个文本文件
subFolder.add(new TextFile("c.txt"));
subFolder.add(new TextFile("d.txt"));
// 打印根文件夹的路径
root.printPath(0);
}
打印结果如下所示:
root
a.txt
b.txt
subFolder
c.txt
d.txt
在使用组合模式时,有几个注意点需要考虑:
假设这样的场景:
在电脑E盘有个文件夹,该文件夹下面有很多文件,有视频文件,有音频文件,有图像文件,还有包含视频、音频及图像的文件夹,十分杂乱,现希望将这些杂乱的文件展示出来。更多内容
不使用组合模式。注:当然可以一个循环遍历就搞定了,因为这里用的是文件的形式,如果是别的形式呢?所以不要太较真了,只是举例。
private void test() {
MusicFile m1 = new MusicFile("尽头.mp3");
MusicFile m2 = new MusicFile("飘洋过海来看你.mp3");
MusicFile m3 = new MusicFile("曾经的你.mp3");
MusicFile m4 = new MusicFile("take me to your heart.mp3");
VideoFile v1 = new VideoFile("战狼2.mp4");
VideoFile v2 = new VideoFile("理想.avi");
VideoFile v3 = new VideoFile("琅琊榜.avi");
ImageFile i1 = new ImageFile("敦煌.png");
ImageFile i2 = new ImageFile("baby.jpg");
ImageFile i3 = new ImageFile("girl.jpg");
Folder aa = new Folder("aa");
aa.addImage(i3);
Folder bb = new Folder("bb");
bb.addMusic(m4);
bb.addVideo(v3);
Folder top = new Folder("top");
top.addFolder(aa);
top.addFolder(bb);
top.addMusic(m1);
top.addMusic(m2);
top.addMusic(m3);
top.addVideo(v1);
top.addVideo(v2);
top.addImage(i1);
top.addImage(i2);
top.print();
}
public class MusicFile {
private String name;
public MusicFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class VideoFile {
private String name;
public VideoFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class ImageFile {
private String name;
public ImageFile(String name){
this.name = name;
}
public void print(){
System.out.println(name);
}
}
public class Folder {
private String name;
//音乐
private List<MusicFile> musicList = new ArrayList<MusicFile>();
//视频
private List<VideoFile> videoList = new ArrayList<VideoFile>();
//图片
private List<ImageFile> imageList = new ArrayList<ImageFile>();
//文件夹
private List<Folder> folderList = new ArrayList<Folder>();
public Folder(String name){
this.name = name;
}
public void addFolder(Folder folder){
folderList.add(folder);
}
public void addImage(ImageFile image){
imageList.add(image);
}
public void addVideo(VideoFile video){
videoList.add(video);
}
public void addMusic(MusicFile music){
musicList.add(music);
}
public void print(){
for (MusicFile music : musicList){
music.print();
}
for (VideoFile video : videoList){
video.print();
}
for(ImageFile image : imageList){
image.print();
}
for (Folder folder : folderList){
folder.print();
}
}
}
如果采用上述的形式,有几个缺点:
为了让系统具有更好的灵活性和可扩展性,客户端可以一致地对待文件和文件夹,定义一个抽象构件AbstractFile,Folder充当容器构件,MusicFile、VideoFile和ImageFile充当叶子构件。更多内容
抽象构件AbstractFile(Component)
public abstract class AbstractFile {
public void add(AbstractFile file) {
throw new UnsupportedOperationException();
}
public void remove(AbstractFile file) {
throw new UnsupportedOperationException();
}
public AbstractFile getChild(int i) {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
叶子构件(Leaf)
public class MusicFile extends AbstractFile {
private String name;
public MusicFile(String name) {
this.name = name;
}
public void print() {
System.out.println(name);
}
}
public class VideoFile extends AbstractFile {
private String name;
public VideoFile(String name) {
this.name = name;
}
public void print() {
System.out.println(name);
}
}
public class ImageFile extends AbstractFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void print() {
System.out.println(name);
}
}
容器构件(Composite)
public class Folder extends AbstractFile {
private String name;
private List<AbstractFile> files = new ArrayList<AbstractFile>();
public Folder(String name) {
this.name = name;
}
@Override
public void add(AbstractFile file) {
files.add(file);
}
@Override
public void remove(AbstractFile file) {
files.remove(file);
}
@Override
public AbstractFile getChild(int i) {
return files.get(i);
}
@Override
public void print() {
for (AbstractFile file : files) {
file.print();
}
}
}
客户端测试:
private void test() {
AbstractFile m1 = new MusicFile("尽头.mp3");
AbstractFile m2 = new MusicFile("飘洋过海来看你.mp3");
AbstractFile m3 = new MusicFile("曾经的你.mp3");
AbstractFile m4 = new MusicFile("take me to your heart.mp3");
AbstractFile v1 = new VideoFile("战狼2.mp4");
AbstractFile v2 = new VideoFile("理想.avi");
AbstractFile v3 = new VideoFile("琅琊榜.avi");
AbstractFile i1 = new ImageFile("敦煌.png");
AbstractFile i2 = new ImageFile("baby.jpg");
AbstractFile i3 = new ImageFile("girl.jpg");
AbstractFile aa = new Folder("aa");
aa.add(i3);
AbstractFile bb = new Folder("bb");
bb.add(m4);
bb.add(v3);
AbstractFile top = new Folder("top");
top.add(aa);
top.add(bb);
top.add(m1);
top.add(m2);
top.add(m3);
top.add(v1);
top.add(v2);
top.add(i1);
top.add(i2);
top.print();
}
用组合模式提供一个抽象构件后,客户端可以一致对待容器构件和叶子构件,进行统一处理,并且大量减少了冗余,扩展性也很好,新增TextFile无需修改Folder源码,只需修改客户端即可。
当然,这里似乎有点违法“迭代器模式”中讲的“单一职责原则”,的确是,抽象构件不但要管理层次结构,还要执行一些业务操作。
组合模式分为透明式的组合模式和安全式的组合模式。这两种类型的主要区别在于抽象构件(Component)角色上的差别。
案例:用组合模式实现在超市购物后,显示并计算所选商品信息与总价。更多内容
案例说明:张三在超市购物,购物清单如下
大袋子的东西如下:
{
1箱牛奶(单价79.8元)
2号小袋子{
3 包山楂(单价7.8元
2包牛肉脯(单价19.8元)
}
中型袋子:{
1盒巧克力(单价39.8元)
1号小袋子:{
2 包芒果干(单价15.8元)
1包薯片(单价9.8元)
}
}
}
透明式的组合模式中抽象构件还声明访问和管理子类的接口
组件(Component): 抽象构建角色。定义一个接口用于计算价格,显示商品,并且可以添加子节点。
public interface Article {
/**
* 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子
*/
void add(Article article);
/**
* 计算价格
*/
Double calculation();
/**
* 显示商品
*/
void show();
}
叶子节点(Leaf): 叶子节点对象,叶子节点没有子节点。创建一个商品类,继承抽象角色。
public class Goods implements Article {
/**
* 商品名称
*/
private String name;
/**
* 购买数量
*/
private Integer quantity;
/**
* 商品单价
*/
private Double unitPrice;
public Goods(String name, Integer quantity, Double unitPrice) {
this.name = name;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
/**
* 树枝构件特有的方法
* 在树叶构件中是能空实现或者抛异常
*/
@Override
public void add(Article article) {
}
@Override
public Double calculation() {
return this.unitPrice * this.quantity;
}
@Override
public void show() {
System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
"合计:" + this.unitPrice * this.quantity + "元");
}
}
复合节点(Composite): 创建袋子
/**
* 树枝构件: 袋子
*/
public class Bag implements Article {
/**
* 袋子名字
*/
private String name;
public Bag(String name) {
this.name = name;
}
/**
* 袋子中的商品
*/
private List<Article> bags = new ArrayList<Article>();
/**
* 往袋子中添加袋子或者商品
*/
@Override
public void add(Article article) {
bags.add(article);
}
@Override
public Double calculation() {
AtomicReference<Double> sum = new AtomicReference<>(0.0);
bags.forEach(e -> {
sum.updateAndGet(v -> v + e.calculation());
});
return sum.get();
}
@Override
public void show() {
bags.forEach(Article::show);
}
}
客户端(Client): 通过组件接口与组合结构进行交互。
private void test() {
Article smallOneBag = new Bag("1号小袋子");
smallOneBag.add(new Goods("芒果干", 2, 15.8));
smallOneBag.add(new Goods("薯片", 1, 9.8));
Article smallTwoBag = new Bag("2号小袋子");
smallTwoBag.add(new Goods("山楂", 3, 7.8));
smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
Article mediumBag = new Bag("中袋子");
mediumBag.add(new Goods("巧克力", 1, 39.8));
mediumBag.add(smallOneBag);
Article BigBag = new Bag("大袋子");
BigBag.add(new Goods("牛奶", 1, 79.8));
BigBag.add(mediumBag);
BigBag.add(smallTwoBag);
System.out.println("打工充选购的商品有:");
BigBag.show();
Double sum = BigBag.calculation();
System.out.println("要支付的总价是:" + sum + "元");
}
以上客户端代码中 new Bag(),new Goods()的引用都是Article,无须区别树叶对象和树枝对象,对客户端来说是透明的,此时Article调用add()是空实现或抛异常的(案例是空实现)。更多内容
安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。
抽象构件(Component)角色。这里创建定义一个接口用于计算价格,显示商品。
public interface Article {
/**
* 计算价格
*/
Double calculation();
/**
* 显示商品
*/
void show();
}
树叶构件(Leaf)角色
public class Goods implements Article {
/**
* 商品名称
*/
private String name;
/**
* 购买数量
*/
private Integer quantity;
/**
* 商品单价
*/
private Double unitPrice;
public Goods(String name, Integer quantity, Double unitPrice) {
this.name = name;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
@Override
public Double calculation() {
return this.unitPrice * this.quantity;
}
@Override
public void show() {
System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
"合计:" + this.unitPrice * this.quantity + "元");
}
}
树枝构件(Composite)角色 / 中间构件
/**
* 树枝构件: 袋子
*/
public class Bag implements Article {
/**
* 袋子名字
*/
private String name;
public Bag(String name) {
this.name = name;
}
/**
* 袋子中的商品
*/
private List<Article> bags = new ArrayList<Article>();
/**
* 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子
* 往袋子中添加袋子或者商品
*/
public void add(Article article) {
bags.add(article);
}
@Override
public Double calculation() {
AtomicReference<Double> sum = new AtomicReference<>(0.0);
bags.forEach(e -> {
sum.updateAndGet(v -> v + e.calculation());
});
return sum.get();
}
@Override
public void show() {
bags.forEach(Article::show);
}
}
客户端(Client): 通过组件接口与组合结构进行交互。
private void test() {
Bag smallOneBag = new Bag("1号小袋子");
smallOneBag.add(new Goods("芒果干", 2, 15.8));
smallOneBag.add(new Goods("薯片", 1, 9.8));
Bag smallTwoBag = new Bag("2号小袋子");
smallTwoBag.add(new Goods("山楂", 3, 7.8));
smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
Bag mediumBag = new Bag("中袋子");
mediumBag.add(new Goods("巧克力", 1, 39.8));
mediumBag.add(smallOneBag);
Bag BigBag = new Bag("大袋子");
BigBag.add(new Goods("牛奶", 1, 79.8));
BigBag.add(mediumBag);
BigBag.add(smallTwoBag);
System.out.println("打工充选购的商品有:");
BigBag.show();
Double sum = BigBag.calculation();
System.out.println("要支付的总价是:" + sum + "元");
}
以上客户端代码中 new Bag(),new Goods()的引用都是Bag,Goods,客户端在调用时要知道树叶对象和树枝对象的存在。此时只有Bag才能调用add()。
在以下情况下可以考虑使用组合模式:
在实际开发过程中,可以对树叶节点和树枝节点分别进行抽象,通过继承的方式让不同的树叶节点和树枝节点子类来实现行为。
在设计时,优先使用接口而非具体类,以提高系统的灵活性和可维护性。更多内容
适用于需要处理复杂树形结构的场景,如文件系统、组织结构等。
01.组合模式基础
组合模式定义 :将对象以树形结构组织起来,以达成“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。
主要解决的问题 :1.简化树形结构中对象的处理,无论它们是单个对象还是组合对象。2.解耦客户端代码与复杂元素的内部结构,使得客户端可以统一处理所有类型的节点。
02.组合模式实现
组合模式包含如下角色:
04.组合实现方式
组合模式分为透明式的组合模式和安全式的组合模式。这两种类型的主要区别在于抽象构件(Component)角色上的差别。
模块 | 描述 | 备注 |
---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 |
23种设计模式
23种设计模式 & 描述 & 核心作用 | 包括 |
---|---|
创建型模式 提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离 | 工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
结构型模式 关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构 | 适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern) |
行为型模式 特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题 | 责任链模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 解释器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 备忘录模式(Memento Pattern) 观察者模式(Observer Pattern) 状态模式(State Pattern) 空对象模式(Null Object Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 访问者模式(Visitor Pattern) |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。