组合模式

组合设计模式UML类图

组合设计模式UML类图

组合设计模式java代码实现

/**
* Word.
*/
public class Word extends LetterComposite {

/**
* Constructor.
*/
public Word(List<Letter> letters) {
letters.forEach(this::add);
}

/**
* Constructor.
* @param letters to include
*/
public Word(char... letters) {
for (char letter : letters) {
this.add(new Letter(letter));
}
}

@Override
protected void printThisBefore() {
System.out.print(" ");
}
}

/**
* Sentence.
*/
public class Sentence extends LetterComposite {

/**
* Constructor.
*/
public Sentence(List<Word> words) {
words.forEach(this::add);
}

@Override
protected void printThisAfter() {
System.out.print(".");
}
}

/**
* Letter.
*/
public class Letter extends LetterComposite {

private char character;

public Letter(char c) {
this.character = c;
}

@Override
protected void printThisBefore() {
System.out.print(character);
}
}

/**
* Composite interface.
*/
public abstract class LetterComposite {

private List<LetterComposite> children = new ArrayList<>();

public void add(LetterComposite letter) {
children.add(letter);
}

public int count() {
return children.size();
}

protected void printThisBefore() {
}

protected void printThisAfter() {
}

/**
* Print.
*/
public void print() {
printThisBefore();
children.forEach(LetterComposite::print);
printThisAfter();
}
}

/**
* Messenger.
*/
public class Messenger {

LetterComposite messageFromOrcs() {

var words = List.of(
new Word('W', 'h', 'e', 'r', 'e'),
new Word('t', 'h', 'e', 'r', 'e'),
new Word('i', 's'),
new Word('a'),
new Word('w', 'h', 'i', 'p'),
new Word('t', 'h', 'e', 'r', 'e'),
new Word('i', 's'),
new Word('a'),
new Word('w', 'a', 'y')
);

return new Sentence(words);

}

LetterComposite messageFromElves() {

var words = List.of(
new Word('M', 'u', 'c', 'h'),
new Word('w', 'i', 'n', 'd'),
new Word('p', 'o', 'u', 'r', 's'),
new Word('f', 'r', 'o', 'm'),
new Word('y', 'o', 'u', 'r'),
new Word('m', 'o', 'u', 't', 'h')
);

return new Sentence(words);

}

}

/**
* The Composite pattern is a partitioning design pattern. The Composite pattern describes that a
* group of objects is to be treated in the same way as a single instance of an object. The intent
* of a composite is to "compose" objects into tree structures to represent part-whole hierarchies.
* Implementing the Composite pattern lets clients treat individual objects and compositions
* uniformly.
*
* <p>In this example we have sentences composed of words composed of letters. All of the objects
* can be treated through the same interface ({@link LetterComposite}).
Composite模式是一种分区设计模式。 复合模式描述了要以与对象的单个实例相同的方式对待一组对象。
组合的目的是将对象“组合”为树状结构,以表示整个部分的层次结构。实施“组合”模式可使客户统一对待各个对象和组合。
<p>在此示例中,我们具有由字母组成的单词组成的句子。 所有对象可以通过相同的界面({@link LetterComposite})进行处理。
*/
public class App {

private static final Logger LOGGER = LoggerFactory.getLogger(App.class);

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {
LOGGER.info("Message from the orcs: ");

var orcMessage = new Messenger().messageFromOrcs();
orcMessage.print();

LOGGER.info("\nMessage from the elves: ");

var elfMessage = new Messenger().messageFromElves();
elfMessage.print();
}
}

应用场景

java.awt中的组合模式

Java GUI分两种:
AWT(Abstract Window Toolkit):抽象窗口工具集,是第一代的Java GUI组件。绘制依赖于底层的操作系统。基本的AWT库处理用户界面元素的方法是把这些元素的创建和行为委托给每个目标平台上(Windows、 Unix、 Macintosh等)的本地GUI工具进行处理。
Swing,不依赖于底层细节,是轻量级的组件。现在多是基于Swing来开发。
我们来看一个AWT的简单示例:

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);
}
}

运行后窗体显示如下
swing
我们在Frame容器中添加了三个不同的构件 Button、Label、TextField,还添加了一个 Panel 容器,Panel 容器中又添加了 Button、Label、TextField 三个构件,为什么容器 Frame 和 Panel 可以添加类型不同的构件和容器呢?
我们先来看下AWT Component的类图
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 源码
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 的类图如下所示
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 对象,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 扮演容器构件角色,其它一般是叶子构件角色