浅谈设计模式 - 组合模式(十二)
浅谈设计模式 - 组合模式(十二)
前言
? 组合模式是一种非常重要的设计模式,使用场景几乎随处可见,各类菜单和目录等地方都能看到组合模式的影子,组合模式通常情况下是和树形结构相辅相成的,而树是软件设计里面非常重要的数据结构,这篇文章将介绍什么是组合模式。
什么是组合模式
? 允许你将对象组合到树形结构表现“整体部分”的结构,组合能让客户以一致的方式处理个别对象和对象组合,组合其实更像是对于对于各种独立组建的“统一性”,可以将一类相似的事物看为一个整体但是拥有完全不同的工作机制。
介绍
? 可以说将相似的物品形成一个集合的模式就是组合模式,他能看两个相似的物品在一处进行完美的融合以及操作。当我们需要 整体/部分的操作时候,就可以使用这种形式。
特点
- 组合模式讲究的是整体和部分之间的关系,整体可以包含部分,部分可以回溯到整体,互相包含
- 组合模式可以让对象结构以“树”的形式包含关系。多数情况可以忽略整体和个体之前的差别
- 组合模式可以帮助对象和组合的对象一视同仁的对待
- 继承结构,修改抽象类违反开放关闭原则
- 如果层次结构非常深,递归结构影响效率
- 使用迭代器有可能造成并发遍历菜单的问题
组合模式以单一职责的原则换取透明性?组合模式的结构图 ? 组合模式的结构图如下:
组合模式破坏了的单一职责原则,组合了多个对象的方法,同时在方法里面做了多种操作,但是这样做却是可以让整个对象可以更加直观的了解整体和部分的特性,这是设计模式里面非常常见的操作。
文章图片
- Component 组件:定义组件的接口,这里可以设计为抽象类,可以设计为接口,可以视为组件的“可能的公共行为”。
- Leaf 叶子节点:用于表示原始对象,叶子节点只需要实现自己的特殊功能即可,比如菜单的菜单子项。
- Composite 组件节点:定义组件行为,可以具备子节点。同时实现叶子节点的相关操作(继承同一个接口),可以视为一个分类的大类
实战 模拟场景 ? 组合模式是为树形结构设计的一种设计模式,案例参照一个菜单的管理功能作为模拟,我们需要拿到不同的菜单分类,在菜单的分类里面,我们有需要拿到不同的菜单项,我们可以由任意的菜单项进入到不同的菜单分类,同时可以进入不同的叶子节点。
? 这次的代码案例是从网上找的例子:
抽象组件
抽象组件定义了组件的通知接口,并实现了增删子组件及获取所有子组件的方法。同时重写了
hashCode
和equales
方法(至于原因,请读者自行思考。如有疑问,请在评论区留言)。package com.jasongj.organization;
import java.util.ArrayList;
import java.util.List;
public abstract class Organization {private List childOrgs = new ArrayList();
private String name;
public Organization(String name) {
this.name = name;
}public String getName() {
return name;
}public void addOrg(Organization org) {
childOrgs.add(org);
}public void removeOrg(Organization org) {
childOrgs.remove(org);
}public List getAllOrgs() {
return childOrgs;
}public abstract void inform(String info);
@Override
public int hashCode(){
return this.name.hashCode();
}@Override
public boolean equals(Object org){
if(!(org instanceof Organization)) {
return false;
}
return this.name.equals(((Organization) org).name);
}}
简单组件(部门)
简单组件在通知方法中只负责对接收到消息作出响应。
package com.jasongj.organization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Department extends Organization{public Department(String name) {
super(name);
}private static Logger LOGGER = LoggerFactory.getLogger(Department.class);
public void inform(String info){
LOGGER.info("{}-{}", info, getName());
}}
复合组件(公司)
复合组件在自身对消息作出响应后,还须通知其下所有子组件
package com.jasongj.organization;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Company extends Organization{private static Logger LOGGER = LoggerFactory.getLogger(Company.class);
public Company(String name) {
super(name);
}public void inform(String info){
LOGGER.info("{}-{}", info, getName());
List allOrgs = getAllOrgs();
allOrgs.forEach(org -> org.inform(info+"-"));
}}
awt的组合模式
? 组合模式因为使用了同样的接口,会让叶子节点实现一些不必要的功能,此时一般可以使用一个 空对象或者使用更为 激进的使用抛出异常的形式。? awt这种老掉牙的东西就不多介绍,java的gui其实就是使用了组合模式,下面是一部分的案例代码:
//创建组件
public MethodsTank() {
//创建组件等
jm = new JMenu("我的菜单(G)");
jmb = new JMenuBar();
jl1 = new JMenuItem("开始新游戏(F)");
jl2 = new JMenuItem("结束游戏");
jl3 = new JMenuItem("重新开始(R)");
jl4 = new JMenuItem("存盘退出");
jl5 = new JMenuItem("回到上次游戏");
draw = new DrawTank();
ses = new selectIsSallup();
//设置快捷键方式
jm.setMnemonic('G');
jl1.setMnemonic('f');
jl3.setMnemonic('r');
jl4.setMnemonic('q');
jl5.setMnemonic('w');
//开启闪烁线程
new Thread(ses).start();
//先运行开始画面
this.addTank();
}public void addTank() {
//添加菜单栏目
jm.add(jl1);
jm.add(jl2);
jm.add(jl3);
jm.add(jl4);
jm.add(jl5);
jmb.add(jm);
//运行选关界面
this.add(ses);
//对于子菜单添加事件
jl1.addActionListener(this);
jl1.setActionCommand("newgame");
jl2.addActionListener(this);
jl2.setActionCommand("gameexit");
jl3.addActionListener(this);
jl3.setActionCommand("restart");
//设置窗体的一些基本属性
this.setTitle("我的坦克大战");
this.setBounds(600, 350, width, height);
//添加菜单栏的方式
this.setJMenuBar(jmb);
this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
this.setVisible(true);
}
总结 ? 组合模式精髓在于“破而后立”,他虽然违反了设计原则,但是通过更加优雅的形式,实现了将单一的对象由部分变为一个整体。
? 而组合模式也经常和适配器模式搭配使用,本文的案例只是一个简单的套板,对于组合模式的实际运用场景其实更常见的情况是关于菜单和菜单子项的内容。
结语 ? 组合模式很多情况下可能并不是十分用的上,更多的时候是和其他的设计模式搭配,组合模式我们需要关注的是“整体-部分”的融合统一即可。
参考资料: ? 这里有一篇讲的更好的资料,在组合模式的基础上给了一个质量稍高的案例代码:
? 实战组合模式「营销差异化人群发券,决策树引擎搭建场景」
源码分析组合模式的典型应用 java.awt中的组合模式 Java GUI分两种:
- AWT(Abstract Window Toolkit):抽象窗口工具集,是第一代的Java GUI组件。绘制依赖于底层的操作系统。基本的AWT库处理用户界面元素的方法是把这些元素的创建和行为委托给每个目标平台上(Windows、 Unix、 Macintosh等)的本地GUI工具进行处理。
- Swing,不依赖于底层细节,是轻量级的组件。现在多是基于Swing来开发。
注意:为了正常显示中文,需要在IDEA中的Edit Configurations -> VM Options
中设置参数-Dfile.encoding=GB18030
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class MyFrame extends Frame {public MyFrame(String title) {
super(title);
}public static void main(String[] args) {
MyFrame frame = new MyFrame("这是一个 Frame");
// 定义三个构件,添加到Frame中去
Button button = new Button("按钮 A");
Label label = new Label("这是一个 AWT Label!");
TextField textField = new TextField("这是一个 AWT TextField!");
frame.add(button, BorderLayout.EAST);
frame.add(label, BorderLayout.SOUTH);
frame.add(textField, BorderLayout.NORTH);
// 定义一个 Panel,在Panel中添加三个构件,然后再把Panel添加到Frame中去
Panel panel = new Panel();
panel.setBackground(Color.pink);
Label lable1 = new Label("用户名");
TextField textField1 = new TextField("请输入用户名:", 20);
Button button1 = new Button("确定");
panel.add(lable1);
panel.add(textField1);
panel.add(button1);
frame.add(panel, BorderLayout.CENTER);
// 设置Frame的属性
frame.setSize(500, 300);
frame.setBackground(Color.orange);
// 设置点击关闭事件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.setVisible(true);
}
}
复制代码
运行后窗体显示如下
文章图片
我们在Frame容器中添加了三个不同的构件
Button
、Label
、TextField
,还添加了一个 Panel
容器,Panel
容器中又添加了 Button
、Label
、TextField
三个构件,为什么容器 Frame
和 Panel
可以添加类型不同的构件和容器呢?我们先来看下AWT Component的类图
文章图片
GUI组件根据作用可以分为两种:基本组件和容器组件。
- 基本组件又称构件,诸如按钮、文本框之类的图形界面元素。
- 容器是一种比较特殊的组件,可以容纳其他组件,容器如窗口、对话框等。所有的容器类都是
java.awt.Container
的直接或间接子类
Container
的部分代码如下public class Container extends Component {
/**
* The components in this container.
* @see #add
* @see #getComponents
*/
private java.util.List component = new ArrayList<>();
public Component add(Component comp) {
addImpl(comp, null, -1);
return comp;
}
// 省略...
}
复制代码
容器父类
Container
内部定义了一个集合用于存储 Component
对象,而容器组件 Container
和 基本组件如 Button
、Label
、TextField
等都是 Component
的子类,所以可以很清楚的看到这里应用了组合模式Component
类中封装了组件通用的方法和属性,如图形的组件对象、大小、显示位置、前景色和背景色、边界、可见性等,因此许多组件类也就继承了 Component
类的成员方法和成员变量,相应的成员方法包括:
getComponentAt(int x, int y)
getFont()
getForeground()
getName()
getSize()
paint(Graphics g)
repaint()
update()
setVisible(boolean b)
setSize(Dimension d)
setName(String name)
复制代码
Java集合中的组合模式
HashMap
提供 putAll
的方法,可以将另一个 Map
对象放入自己的存储空间中,如果有相同的 key 值则会覆盖之前的 key 值所对应的 value 值public class Test {
public static void main(String[] args) {
Map map1 = new HashMap();
map1.put("aa", 1);
map1.put("bb", 2);
map1.put("cc", 3);
System.out.println("map1: " + map1);
Map map2 = new LinkedMap();
map2.put("cc", 4);
map2.put("dd", 5);
System.out.println("map2: " + map2);
map1.putAll(map2);
System.out.println("map1.putAll(map2): " + map1);
}
}
复制代码
输出结果
map1: {aa=1, bb=2, cc=3}
map2: {cc=4, dd=5}
map1.putAll(map2): {aa=1, bb=2, cc=4, dd=5}
复制代码
查看
putAll
源码public void putAll(Map extends K, ? extends V> m) {
putMapEntries(m, true);
}
复制代码
putAll
接收的参数为父类 Map
类型,所以 HashMap
是一个容器类,Map
的子类为叶子类,当然如果 Map
的其他子类也实现了 putAll
方法,那么它们都既是容器类,又都是叶子类同理,
ArrayList
中的 addAll(Collection extends E> c)
方法也是一个组合模式的应用,在此不做探讨Mybatis SqlNode中的组合模式 MyBatis 的强大特性之一便是它的动态SQL,其通过
if
, choose
, when
, otherwise
, trim
, where
, set
, foreach
标签,可组合成非常灵活的SQL语句,从而提高开发人员的效率。来几个官方示例:
动态SQL -- IF
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
AND title like #{title}
AND author_name like #{author.name}
复制代码
动态SQL -- choose, when, otherwise
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
AND title like #{title}
AND author_name like #{author.name}
AND featured = 1
复制代码
动态SQL -- where
SELECT * FROM BLOG
state = #{state}
AND title like #{title}
AND author_name like #{author.name}
复制代码
【浅谈设计模式 - 组合模式(十二)】动态SQL -- foreach
SELECT * FROM POST P WHERE ID in
#{item}
复制代码
Mybatis在处理动态SQL节点时,应用到了组合设计模式,Mybatis会将映射配置文件中定义的动态SQL节点、文本节点等解析成对应的 SqlNode 实现,并形成树形结构。
SQLNode
的类图如下所示文章图片
需要先了解
DynamicContext
类的作用:主要用于记录解析动态SQL语句之后产生的SQL语句片段,可以认为它是一个用于记录动态SQL语句解析结果的容器抽象构件为
SqlNode
接口,源码如下public interface SqlNode {
boolean apply(DynamicContext context);
}
复制代码
apply
是 SQLNode
接口中定义的唯一方法,该方法会根据用户传入的实参,参数解析该SQLNode所记录的动态SQL节点,并调用 DynamicContext.appendSql()
方法将解析后的SQL片段追加到 DynamicContext.sqlBuilder
中保存,当SQL节点下所有的 SqlNode
完成解析后,我们就可以从 DynamicContext
中获取一条动态生产的、完整的SQL语句然后来看
MixedSqlNode
类的源码public class MixedSqlNode implements SqlNode {
private List contents;
public MixedSqlNode(List contents) {
this.contents = contents;
}@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
复制代码
MixedSqlNode
维护了一个 List
类型的列表,用于存储 SqlNode
对象,apply
方法通过 for循环
遍历 contents 并调用其中对象的 apply
方法,这里跟我们的示例中的 Folder
类中的 print
方法非常类似,很明显 MixedSqlNode
扮演了容器构件角色对于其他SqlNode子类的功能,稍微概括如下:
TextSqlNode
:表示包含${}
占位符的动态SQL节点,其 apply 方法会使用GenericTokenParser
解析${}
占位符,并直接替换成用户给定的实际参数值IfSqlNode
:对应的是动态SQL节点
节点,其 apply 方法首先通过ExpressionEvaluator.evaluateBoolean()
方法检测其 test 表达式是否为 true,然后根据 test 表达式的结果,决定是否执行其子节点的 apply() 方法TrimSqlNode
:会根据子节点的解析结果,添加或删除相应的前缀或后缀。WhereSqlNode
和SetSqlNode
都继承了TrimSqlNode
ForeachSqlNode
:对应
标签,对集合进行迭代- 动态SQL中的
、
、
分别解析成ChooseSqlNode
、IfSqlNode
、MixedSqlNode
SqlNode
接口有多个实现类,每个实现类对应一个动态SQL节点,其中 SqlNode
扮演抽象构件角色,MixedSqlNode
扮演容器构件角色,其它一般是叶子构件角色推荐阅读
- 21天|21天|羊多多组合《书都不会读,你还想成功》
- 设计模式-代理模式-Proxy
- 2018-08-29|2018-08-29 - 草稿 - 草稿
- 性能测试中QPS和TPS的区别
- R语言|R语言 函数
- 设计模式【15】--从审批流中学习责任链模式
- 浅谈教育与医学之本质和医学生培养之三观
- 浅谈朋友圈
- 2020-11-18迭代器设计模式
- 急需解答一个组合题