共计 9592 个字符,预计需要花费 24 分钟才能阅读完成。
浅谈设计模式 – 组合模式(十二)
前言
组合模式是一种十分重要的设计模式,应用场景简直随处可见,各类菜单和目录等中央都能看到组合模式的影子,组合模式通常状况下是和树形构造相辅相成的,而树是软件设计外面十分重要的数据结构,这篇文章将介绍什么是组合模式。
什么是组合模式
容许你将对象组合到树形构造体现“整体局部”的构造,组合能让客户以统一的形式解决个别对象和对象组合,组合其实更像是对于对于各种独立组建的“统一性”,能够将一类类似的事物看为一个整体然而领有齐全不同的工作机制。
介绍
能够说将类似的物品造成一个汇合的模式就是组合模式,他能看两个类似的物品在一处进行完满的交融以及操作。当咱们须要 整体 / 局部 的操作时候,就能够应用这种模式。
特点
- 组合模式考究的是整体和局部之间的关系,整体能够蕴含局部,局部能够回溯到整体,相互蕴含
- 组合模式能够让对象构造以“树”的模式蕴含关系。少数状况能够疏忽整体和个体之前的差异
优缺点
长处:
- 组合模式能够帮忙对象和组合的对象厚此薄彼的看待
毛病:
- 继承构造,批改抽象类违反凋谢敞开准则
- 如果层次结构十分深,递归结构影响效率
- 应用迭代器有可能造成并发遍历菜单的问题
组合模式以繁多职责的准则换取透明性?
组合模式毁坏了的繁多职责准则,组合了多个对象的办法,同时在办法外面做了多种操作,然而这样做却是能够让整个对象能够更加直观的理解整体和局部的个性,这是设计模式外面十分常见的操作。
组合模式的结构图
组合模式的结构图如下:
- Component 组件:定义组件的接口,这里能够设计为抽象类,能够设计为接口,能够视为组件的“可能的公共行为”。
- Leaf 叶子节点:用于示意原始对象,叶子节点只须要实现本人的非凡性能即可,比方菜单的菜单子项。
- Composite 组件节点:定义组件行为,能够具备子节点。同时实现叶子节点的相干操作(继承同一个接口),能够视为一个分类的大类
理论利用场景
因为事实场景当中这样的设计模式构造是有树状构造转换而来的,所以组合模式的应用场景就是呈现树形构造的中央。比方:文件目录显示 ,多及目录出现等 树形构造数据 的操作。上面咱们就应用一个菜单的构造来理解一下组合模式的“模板”代码。
实战
模仿场景
组合模式是为树形结构设计的一种设计模式,案例参照一个菜单的治理性能作为模仿,咱们须要拿到不同的菜单分类,在菜单的分类外面,咱们有须要拿到不同的菜单项,咱们能够由任意的菜单项进入到不同的菜单分类,同时能够进入不同的叶子节点。
这次的代码案例是从网上找的例子:
形象组件
形象组件定义了组件的告诉接口,并实现了增删子组件及获取所有子组件的办法。同时重写了 hashCode
和equales
办法(至于起因,请读者自行思考。如有疑难,请在评论区留言)。
package com.jasongj.organization; | |
import java.util.ArrayList; | |
import java.util.List; | |
public abstract class Organization {private List<Organization> childOrgs = new ArrayList<Organization>(); | |
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<Organization> 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<Organization> 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 来开发。
咱们来看一个 AWT 的简略示例:
留神:为了失常显示中文,须要在 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> 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<String, Integer> map1 = new HashMap<String, Integer>(); | |
map1.put("aa", 1); | |
map1.put("bb", 2); | |
map1.put("cc", 3); | |
System.out.println("map1:" + map1); | |
Map<String, Integer> 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 id="findActiveBlogLike" resultType="Blog"> | |
SELECT * FROM BLOG WHERE state =‘ACTIVE’<if test="title != null"> | |
AND title like #{title} | |
</if> | |
<if test="author != null and author.name != null"> | |
AND author_name like #{author.name} | |
</if> | |
</select> | |
复制代码 |
动静 SQL — choose, when, otherwise
<select id="findActiveBlogLike" resultType="Blog"> | |
SELECT * FROM BLOG WHERE state =‘ACTIVE’<choose> | |
<when test="title != null"> | |
AND title like #{title} | |
</when> | |
<when test="author != null and author.name != null"> | |
AND author_name like #{author.name} | |
</when> | |
<otherwise> | |
AND featured = 1 | |
</otherwise> | |
</choose> | |
</select> | |
复制代码 |
动静 SQL — where
<select id="findActiveBlogLike" resultType="Blog"> | |
SELECT * FROM BLOG | |
<where> | |
<if test="state != null"> | |
state = #{state} | |
</if> | |
<if test="title != null"> | |
AND title like #{title} | |
</if> | |
<if test="author != null and author.name != null"> | |
AND author_name like #{author.name} | |
</if> | |
</where> | |
</select> | |
复制代码 |
动静 SQL — foreach
<select id="selectPostIn" resultType="domain.blog.Post"> | |
SELECT * FROM POST P WHERE ID in | |
<foreach item="item" index="index" collection="list" | |
open="(" separator="," close=")"> | |
#{item} | |
</foreach> | |
</select> | |
复制代码 |
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<SqlNode> contents; | |
public MixedSqlNode(List<SqlNode> contents) {this.contents = contents;} | |
@Override | |
public boolean apply(DynamicContext context) {for (SqlNode sqlNode : contents) {sqlNode.apply(context); | |
} | |
return true; | |
} | |
} | |
复制代码 |
MixedSqlNode
保护了一个 List<SqlNode>
类型的列表,用于存储 SqlNode
对象,apply
办法通过 for 循环
遍历 contents 并调用其中对象的 apply
办法,这里跟咱们的示例中的 Folder
类中的 print
办法十分相似,很显著 MixedSqlNode
表演了容器构件角色
对于其余 SqlNode 子类的性能,略微概括如下:
TextSqlNode
:示意蕴含${}
占位符的动静 SQL 节点,其 apply 办法会应用GenericTokenParser
解析${}
占位符,并间接替换成用户给定的理论参数值IfSqlNode
:对应的是动静 SQL 节点<If>
节点,其 apply 办法首先通过ExpressionEvaluator.evaluateBoolean()
办法检测其 test 表达式是否为 true,而后依据 test 表达式的后果,决定是否执行其子节点的 apply() 办法TrimSqlNode
:会依据子节点的解析后果,增加或删除相应的前缀或后缀。WhereSqlNode
和SetSqlNode
都继承了TrimSqlNode
ForeachSqlNode
:对应<foreach>
标签,对汇合进行迭代- 动静 SQL 中的
<choose>
、<when>
、<otherwise>
别离解析成ChooseSqlNode
、IfSqlNode
、MixedSqlNode
综上,SqlNode
接口有多个实现类,每个实现类对应一个动静 SQL 节点,其中 SqlNode
表演形象构件角色,MixedSqlNode
表演容器构件角色,其它个别是叶子构件角色