Java 设计模式——组合模式

概述

有时我们可能会被要求处理一个层级结构明显的对象,比如上下级的公司员工、比如层级嵌套的文件夹,还有丰富多彩的美食菜单。可是,我们可能要屡试不爽地编写深度搜索代码、要小心翼翼地编写递归逻辑。现在你可以忘掉这些,学习一些新的技能,让你秒刷副本。当然,这句有些夸张,你可以忽略。只是它单纯地表达我对本文要说的这个模式的喜欢(也有可能只是因为我工作中恰好遇到这个问题)。


组合模式

定义

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

情境分析

看到组合的模式的定义,或许你就知道了这里的组合与我们平常讨论的*“继承-组合”*并不是同一个概念。因为在学习组合模式之前,我就是把这两个弄得有点混淆,所以一直没有认真地学习过它,以为组合也就是那么回事了嘛。可是,当我开始真的学习它的时候,才知道,这是两回事。
定义上说的是部分与整体的层次结构,可能就这前半句还不能说明什么,不过我们可以从后半句中找到突破点,那就是单个对象与组合对象。也就是在组合对象中组合了一个部分的集合,这个集合中的元素也就是单个元素所具有的对象类型。
当然,你不要理解成组合模式里也可以只有这个集合了。如果是这样,那么一个 List 完全可以可以搞定,又何必搞出个模式来突显逼格呢?
现在我们就来“举个栗子”,对,是栗子。哈哈~
我现在是一家中式餐厅的老板了。在我的店里你可以享用早餐、中餐、晚餐。早餐就简单点,只提供包子、油条跟豆浆吧(是的,这个不是主业)。午餐会丰盛一些,在午餐里你们选择北方菜或是南方菜。北方菜主要有:锅包肉、猪肉炖粉条、辣子鸡、干炸里脊、糖醋里脊、板栗烧鸡、地三鲜、红烧肉、回锅肉、 口水鸡、宫保鸡丁、可乐鸡翅;南方菜主要有:炒米粉、南瓜饼、南焖羊肉、蒸排骨、肉片炒青椒、水果皮萨、北炒鱼香茄子、糯米糍团、芥菜煲。晚餐上可以吃得精致一些,主要有地道小吃和甜点。地道小吃:肉夹馍、羊肉泡馍、乌冬面、章鱼小丸子、葱油饼、老婆饼;甜点:冰淇淋、鲜奶蛋糕还有蜜汁藕。
把上面的描述转换成图片菜单就像下面这样的:

这里写图片描述

当然,这里并不够齐全,只是为了照顾图片的大小(当然也不可否认是博主偷懒了),只绘制了其中的一部分,不过足够我们说明问题了。

一般组合模式

模式分析

在一般组合模式里,我们只做了一件事,那就是模糊了简单元素与复杂元素。怎么说明这一点呢?针对上面举的例子来说,我们的每一种菜都是一个简单元素,而每一种菜系(南方/北方/小说/甜点)或是餐饮的类型(早餐/午餐/晚餐)都是一个复杂元素,因为这里又包含若干的简单元素。我们把菜系定义为 Menu,而每一道具体的菜则定义成 MenuItem。这样我们就可以绘制出一般组合模式的类图,如下:

这里写图片描述

这里面我们的菜单(Menu)和菜单项(MenuItem)都继承自一个 MenuComponent。MenuComponent 是一个抽象的类。当 Menu 和 MenuItem 继承自同一个类时,我们就可以实现模糊简单元素与复杂元素了,因为我们可以按照处理 MenuComponent 的方式处理 Menu 和 MenuItem。这里我们只是模糊了这两个的分界,却不能真的等同看待。很简单,上面的的 MenuItem 最起码有一个价格的属性、而 Menu 就不存在这个属性;Menu 可以有一个 add MenuComponent 的方法,而 MenuItem 则不可能会有。
说到这里,可能你会说组合模式并不完美。是的,我也这么觉得。它让这件事情模糊了,让 MenuComponent 的使用产生了歧义。比如我们在使用它的时候,根本不知道它的某一个方法是可以正常使用。比如,一个 MenuComponent(可实际的类型可能是 Menu,而我们不知道),这时可能会调用它的 getPrice() 方法,这是有问题的,逻辑上是走不通的。那么,我们就必须要为 Menu 的这个方法抛出一个异常(异常是一个心机 boy,我们都不太喜欢它)。当然,也可以在外部使用 instanceof 关键字处理。可是,这样的处理总让我有一种非面向对象的处理过程,所以还是抛出异常吧。
另外,Menu 中组合了 MenuItem,从这一点来看,倒是有几分“继承-组合”的意味。

逻辑实现

先来看看抽象类,这个是基础:
MenuComponent.java


public abstract class MenuComponent {public String getName() {throw new UnsupportedOperationException("暂不支持此操作");}public String getDescription() {throw new UnsupportedOperationException("暂不支持此操作");}public double getPrice() {throw new UnsupportedOperationException("暂不支持此操作");}public boolean isVegetarian() {throw new UnsupportedOperationException("暂不支持此操作");}public void print() {throw new UnsupportedOperationException("暂不支持此操作");}public void add(MenuComponent menuComponent) {throw new UnsupportedOperationException("暂不支持此操作");}public void remove(MenuComponent menuComponent) {throw new UnsupportedOperationException("暂不支持此操作");}public MenuComponent getChild(int childIndex) {throw new UnsupportedOperationException("暂不支持此操作");}
}

而在 Menu 的具体类中,虽然是继承了 MenuComponent,可是它的抽象方法又不能全部重写。原因上面也说了,这里不赘述了。可是,由于 Java 语法的客观存在,所以这里我们抛出了一个异常。
Menu.java

public class Menu extends MenuComponent {private String name = null;private String desc = null;private List<MenuComponent> menuComponents = null;public Menu(String _name, String _desc) {name = _name;desc = _desc;}@Overridepublic String getName() {return name;}@Overridepublic String getDescription() {return desc;}@Overridepublic void print() {System.out.println("\nMenu: { " + name + ", " + desc + " }");if (menuComponents == null) {return;}System.out.println("-------------------------");for (MenuComponent menuComponent : menuComponents) {menuComponent.print();}}@Overridepublic void add(MenuComponent menuComponent) {if (menuComponents == null) {menuComponents = new ArrayList<MenuComponent>();}menuComponents.add(menuComponent);}@Overridepublic MenuComponent getChild(int childIndex) {if (menuComponents == null || menuComponents.size() <= childIndex) {return null;}return menuComponents.get(childIndex);}
}

基于上面对 Menu 类的说明,这里的 MenuItem 类的实现过程也是一样:只重写能够重写的部分,不能重写的地方抛出一个异常等待上层处理。
MenuItem.java

public class MenuItem extends MenuComponent {private String name = null;private String desc = null;private boolean vegetarian = false;private double price = 0.0d;public MenuItem(String _name, String _desc, boolean _vegetarian, double _price) {this.name = _name;this.desc = _desc;this.vegetarian = _vegetarian;this.price = _price;}@Overridepublic String getName() {return name;}@Overridepublic String getDescription() {return desc;}@Overridepublic double getPrice() {return price;}@Overridepublic boolean isVegetarian() {return vegetarian;}@Overridepublic void print() {System.out.println("MenuItem: { " + name + ", " + desc + ", " + vegetarian + ", " + price + " }");}
}

上面的代码是整个一般组合模式的关键部分,这是需要注意的是它们的 print() 方法。对于 MenuItem 的 print() 来说,是很常规的打印,而 Menu 的打印则需要作处理。因为我们正常的理解里一个菜单因为会包含很多菜单项,所以,这里我们就把当前菜单下的所以菜单打印一遍。不过,这也不是什么难事,因为在每份 Menu 中都有一个 MenuItem 的列表。好了,问题解决。详情参见上面的代码部分。

模式小结

从上面的例子也可以看出,组合模式在解决有层级关系时,有着得天独厚的优势。思路清晰、代码优雅。唯一的不足是我们要针对不同的情况抛出相应的异常。

组合与迭代

对于组合模式息息相关的另一种模式——迭代模式,它在组合模式中可以说有着重要的地位。在上面的代码中,有点编程逻辑的人应该都可以发现,它们的 print() 方法是对象内部的操作。也就是说,如果我想要通过一个 Menu 操作一个 MenuItem 就必须在 Menu 内部进行实现。这是不现实的,因为需求变化的速度,可能隔了几秒连它的亲妈也不认识了。正因为如此,所以我们就必须想办法从外部拿到 Menu 中的 MenuItem。
比如现在我想知道餐厅里所有的素食有哪些,如果我们不去改动原有代码,那么就可以添加一个外部的迭代逻辑。

深搜的试水之行

或许你又会说,这里根本不需要使用迭代,用一次深搜就 OK 了。是的没错,而且对于一个数据结构基本功还可以的同学,可以马上写出一个深搜的解决方案。这里给出我的深搜方案:

public void showVegetarMenu(MenuComponent menu) {List<MenuComponent> visited = new ArrayList<>();showVegetarMenu(menu, visited);}private void showVegetarMenu(MenuComponent menu, List<MenuComponent> visited) {if (visited.contains(menu)) {return;}if (menu instanceof MenuItem) {if (menu.isVegetarian()) {System.out.println(menu);}return;}List<MenuComponent> children = ((Menu) menu).getChildren();for (int i = 0; i < children.size(); i++) {showVegetarMenu(children.get(i), visited);}}

结果不出意外。能够使用深搜,已然是逼格满满了。不过,使用深搜让我有一种面向过程编程的感觉,不够优雅。下面就让我用迭代器来实现一次华丽的逆转吧。

迭代的逆转

首先我们为 MenuComponent 添加一个 createIterator() 方法。就像下面这样:
MenuComponent.java

public abstract class MenuComponent {( ... 省略重复的 N... )public abstract Iterator<MenuComponent> createIterator();
}

由于这里添加的是一个抽象的方法,那么在 Menu 和 MenuItem 中就要必须重写这个 createIterator() 方法。
Menu.java

public class Menu extends MenuComponent {private CompositeIterator iterator = null;( ... 省略重复的 N... )@Overridepublic Iterator<MenuComponent> createIterator() {if (iterator == null) {iterator = new CompositeIterator(menuComponents.iterator());}return iterator;}
}

MenuItem.java

public class MenuItem extends MenuComponent {( ... 省略重复的 N... )@Overridepublic Iterator<MenuComponent> createIterator() {return new NullIterator();}
}

在上面两段代码中提到了两个迭代器类:CompositeIterator、NullIterator。这里有参照书本上的逻辑,不过也有改动,因为书本的迭代器没有通用性,下面会对这一点进行说明的。
CompositeIterator.java

public class CompositeIterator implements Iterator<MenuComponent> {private Stack<Iterator> stack = new Stack<>();public CompositeIterator(Iterator iterator) {stack.push(iterator);}@Overridepublic boolean hasNext() {if (stack.empty()) {return false;}Iterator iterator = stack.peek();if (!iterator.hasNext()) {stack.pop();return hasNext();}return true;}@Overridepublic MenuComponent next() {if (hasNext()) {Iterator iterator = stack.peek();MenuComponent component = (MenuComponent) iterator.next();if (component instanceof Menu) {Iterator menuIterator = component.createIterator();if (!stack.contains(menuIterator)) {stack.push(menuIterator);}}return component;}return null;}@Overridepublic void remove() {throw new UnsupportedOperationException();}
}

这里的栈结构使用得很巧妙,因为这个栈的使用让我想到在 LeetCode 上的一道算法题,也是使用栈来实现,而且比一般的算法复杂度低很多,如果我不犯懒的话,应该会写那一篇博客的。咳咳,扯远了,回到正题。有关于栈的使用是一些数据结构和 Java api 的基础,这里不多说什么了。还有这里的 hasNext() 和 next() 方法,这里要求你对数据结构和 Java api(主要是 Stack 这一块)比较熟悉。所以,如果你看到这个地方有什么不太理解的,可以留言,也可以自行复习一下这两块内容。

NullIterator.java

public class NullIterator implements Iterator<MenuComponent> {@Overridepublic boolean hasNext() {return false;}@Overridepublic MenuComponent next() {return null;}@Overridepublic void remove() {throw new UnsupportedOperationException();}
}

因为每个菜单项都不可能什么子菜单项,也就不存在什么迭代器了,所以在 MenuItem 中就可以返回一个 Null 的迭代器。当然,这是理想的做法。你也可以直接返回 null,只是这样一来,在上层就要多一次判空处理,相比较而言,这样的实现更优雅。
程序的结果自然不出所料:

素食菜单(迭代)
MenuItem: { 包子, bun, true, 1.5 }
MenuItem: { 油条, fritters, true, 1.2 }
MenuItem: { 豆浆, milk, true, 2.0 }
MenuItem: { 炒米粉, Fried noodles, true, 8.0 }
MenuItem: { 冰淇淋, ice cream, true, 5.0 }

只是,如果你只采用书本上的迭代器来实现,就会出现多级菜单下的菜单项被 show 了 N 遍。而你只能一脸懵逼。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/555689.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于海康官网接口文档中:取流URL有效时间为5分钟表述歧义的说明

在海康官方在线接口文档中&#xff08;原文链接&#xff1a;https://open.hikvision.com/docs/63f761576c594a309708525e1eefdbdb&#xff09;&#xff0c;关于视频预览接口中&#xff1a;获取监控点预览取流URLv2 &#xff0c;这个接口的接口说明第6条为保证数据的安全性&…

调用海康视频接口获取预览取流的URL

开始之前请参考海康官方SDK文档 鉴于前段时间刚接触视频这一块&#xff0c;整理了自己的一些经验&#xff0c;具体的你们还是参考文档来吧 附上文档地址 https://open.hikvision.com/docs/docId?productId5c67f1e2f05948198c909700&curNodeId16741aecc05944a6b0cd1341d68…

c if语句多个条件判断顺序_Java中的流程控制语句 (基础篇四)

流程控制就是对事物次序的布置和安排,在程序中就是对代码执行次序的安排和控制程序中的流程控制主要有三种&#xff1a;顺序流程、选择流程、循环流程。顺序流程&#xff1a;比如打印输出的代码按照指定的顺序结构依次排序&#xff0c;打印的结果按照代码的顺序执行打印&#x…

git checkout 会把改动带过去吗_原创 | 操作失误不要慌,这个命令给你的Git一次反悔的机会...

点击上方蓝字&#xff0c;关注并星标&#xff0c;和我一起学技术。今天我们来介绍git当中两个非常非常好用的工具&#xff0c;git show和reflog。这两个命令虽然不是必知必会&#xff0c;但是如果熟练使用可以极大地帮助我们查看代码仓库的问题&#xff0c;以及在我们操作失误的…

计算机指令执行与时序逻辑,时序逻辑系统

时序逻辑电路其任一时刻的输出不仅取决于该时刻的输入&#xff0c;而且还与过去各时刻的输入有关。常见的时序逻辑电路有触发器、计数器、寄存器等。时序逻辑电路在逻辑功能上的特点是任意时刻的输出不仅取决于当时的输入信号&#xff0c;而且还取决于电路原来的状态&#xff0…

flume连接kafka_日志收集系统架构设计:(flume+zookeeper+kafka+php+mysql )

正文内容一、安装jdk二、安装flume三、安装kafka1、zookeeper2、kafka四、启动测试步骤五、目录说明六、日志收集系统设计图七、大数据参考资料推荐一、安装jdk -(版本&#xff1a;1.8.0_191)1.下载&#xff1a;https://www.oracle.com/technetwork/java/javase/downloads/jdk8…

2020班徽设计图案高中计算机,高铁工程学院举办2020级班徽设计大赛

大赛现场12月3日&#xff0c;高铁工程学院团总支第四届“班徽设计”大赛在GB250教室隆重举行&#xff0c;高铁工程学院20级新生各班参赛&#xff0c;20级辅导员代表出席评委席并参与评分。获奖选手领取证书本次比赛采用了线下评分及线上投票相结合的方式。比赛开始&#xff0c;…

Web浏览器没有Flash如何播放RTMP协议直播

各大主流浏览器在很早的时候就已声明 2020 年底不支持 Adobe Flash。所以已经线上运行的项目以及涉及直播的项目&#xff0c;都会涉及一个问题 &#xff1a; “没有 Adobe Flash 在 Web 浏览器端如何播放 RTMP 直播流&#xff1f;” 还好有先见之明&#xff0c;我参与涉及直播的…

video-js RTMP直播

目前主流的几种直播协议 协议传输方式视频封装格式延时数据分段html播放httpflvhttpflv低连续可通过html5解封包播放(flv.js)rtmptcpflv tag低连续不支持dashhttpts文件高切片可通过html5解封包播放(hls.js)hls$1mp4 3gp webm高切片如果dash文件列表是mp4webm文件&#xff0c;…

EJB到底是什么?(通俗易懂白话文)

1. 我们不禁要问&#xff0c;什么是"服务集群"&#xff1f;什么是"企业级开发"&#xff1f; 既然说了EJB 是为了"服务集群"和"企业级开发"&#xff0c;那么&#xff0c;总得说说什么是所谓的"服务 集群"和"企业级开发&…

ABC334 A-F

打的很懒的一场B卡了D看不懂题卡了F没看完题目理解错题意了&#xff0c;状态好差XD UNIQUE VISION Programming Contest 2023 Christmas (AtCoder Beginner Contest 334) - AtCoder A - Christmas Present 题意&#xff1a; 给出两个数B, G问哪个大 题解&#xff1a; 凑数…

华为笔记本matebook13_华为引领“第三代移动办公”新纪元 华为MateBook开启“智慧化办公”新赛道...

&#xfeff;运营商财经网 康钊/文移动互联网的快速兴起&#xff0c;让办公形式不再受时间、地点的限制&#xff0c;笔记本电脑、平板电脑、手机等承担生产力工具作用的电子设备也是越来越多样化&#xff0c;“移动办公”正成为一种不断演化市场趋势。然而&#xff0c;随着移动…

IPv4地址和IPv6地址的比较,IPv6地址及其表示

IPv4地址和IPv6地址的比较&#xff0c;IPv6地址及其表示 TCP/IP协议是互联网发展的基石&#xff0c;其中IP是网络层协议&#xff0c;规范互联网中分组信息的交换和选路。目前采用的IPv4协议地址长度为32位&#xff0c;总数约43亿个IPv4地址已分配殆尽。 IPv6是IP地址的第六版…

MySQL如何有效的存储IP地址

文章目录序言工具类实现转换数据库函数实现转换一、IP地址应该怎么存二、整数存储 IP 地址的查询性能实验1、测试范围查询&#xff1a;2、IP精确查询&#xff1a;3、整理一下结果发现&#xff1a;总结首先就来阐明一下部分人得反问&#xff1a;为什么要问IP得知怎样存&#xff…

ab753变频器参数怎么拷贝到面板_变频器怎么设置参数?变频器的基本参数设定...

电工学习网&#xff1a;www.diangon.com技术驱动未来&#xff0c;关注电工学习网官方微信公众号“电工电气学习”&#xff0c;收获更多经验知识。变频器在工业生产中应用及其重要&#xff0c;其除了调速&#xff0c;软启动作用外&#xff0c;最重要的是可以节能。变频器功能参数…

卢克增加服务器,DNF卢克跨区服务器崩溃?策划:暗制造者临时加入安图恩攻坚...

原标题&#xff1a;DNF卢克跨区服务器崩溃&#xff1f;策划&#xff1a;暗制造者临时加入安图恩攻坚DNF作为一款即时在线&#xff0c;2d网络游戏&#xff0c;决斗场系统延伸到早期60级版本组队刷图。玩家与玩家之间互动&#xff0c;即时刷图跨区&#xff0c;成为了阿拉德大陆一…

使用TortoiseGit(小乌龟)操作分支的创建

现在的我的github库上面只有一个master分支 由于是穷屌丝用不起mac&#xff0c;所以我windows为例进行相应的演示&#xff1a; 下图就是本地使用小乌龟的版本管理工具在只有一个分支的情况下执行命令&#xff1a;Switch/Checkout 显示的内容。 下面分为三个步骤进行创建分支操…

计算机丢失d3dcompile,电脑d3dcompiler43.dll文件丢失怎么办 文件丢失解决方法

最近有位用户私信给小编&#xff0c;说他在使用电脑的时候&#xff0c;电脑一直弹出丢失d3dcompiler43.dll文件的弹窗。就算是关闭&#xff0c;下次开机依旧会弹出这样的窗口&#xff0c;十分厌烦。那丢失电脑中的d3dcompiler43.dll文件应该怎么办呢&#xff1f;其实也不难&…

mybatis plus 导出sql_软件更新丨mybatis-plus 3.0.7 发布,辞旧迎新

点击右上方&#xff0c;关注开源中国OSC头条号&#xff0c;获取最新技术资讯Mybatis-Plus 是一款 Mybatis 动态 SQL 自动注入 Mybatis 增删改查 CRUD 操作中间件&#xff0c; 减少你的开发周期优化动态维护 XML 实体字段&#xff0c;无入侵全方位 ORM 辅助层让您拥有更多时间陪…

网站服务器怎么用手机登录不了怎么办,怎么打不开服务器列表了?

2011-04-19网页图片打不开是不是中毒呢&#xff1f;以前网页中没有图片、视频、动画、声音。打开IE选工具/Internet选项/高级/在设置的下拉列表&#xff0c;勾选“播放网页中的动画”“播放网页中的声音”“播放网页中的视频”“显示图片”(也可以直接选择高级中下面的“还原默…