【设计模式系列】组合模式(十二)
目录
一、什么是组合模式
二、组合模式的角色
三、组合模式的典型应用
四、组合模式在Mybatis SqlNode中的应用
4.1 XML映射文件案例
4.2 Java代码使用案例
一、什么是组合模式
组合模式(Composite Pattern)是一种结构型设计模式,其核心思想是将对象组合成树状结构,使得单个对象和对象的组合能够以相同的方式被处理。这种模式提供了一个将对象表示为部分-整体层次结构的方法,允许客户端对单个对象和组合对象的使用具有一致性。
二、组合模式的角色
-
Component(抽象构件):
- 作用:定义了对象结构的公共接口,包括业务方法和在需要时访问和管理其子构件的方法(如
add
、remove
、getChild
)。这个接口可以是抽象类或接口。 - 细节:Component作为组合中的对象的公共类或接口,使得叶子构件和组合构件可以被统一对待。
- 作用:定义了对象结构的公共接口,包括业务方法和在需要时访问和管理其子构件的方法(如
-
Leaf(叶子构件):
- 作用:表示对象结构中的叶节点,没有子构件。叶子构件实现了抽象构件定义的接口,但是其add或remove操作通常不做任何事情(可能是抛出异常或者简单返回)。
- 细节:Leaf对象通常包含实现细节,因为它们不包含子构件,所以它们是实际执行业务逻辑的末端对象。
-
Composite(组合构件):
- 作用:表示对象结构中的复合节点,它可以包含子构件。Composite对象存储子构件集合,并实现在抽象构件中定义的方法来管理子构件。
- 细节:Composite对象实现了添加和删除子构件的方法,并且通常会递归地调用其子构件的业务方法,以确保整个结构的一致性。
三、组合模式的典型应用
-
构建树形结构:任何需要表示部分-整体层次结构的场景,如组织架构、类目体系等。
-
创建复杂对象:在需要构建复杂对象,而这些对象由更简单的对象组成时,如构建一个由多个部件组成的汽车对象。
-
处理递归结构:当需要处理递归结构的数据时,如遍历、搜索、排序等操作。
-
实现插件架构:在需要构建一个可扩展的插件架构时,可以使用组合模式来表示插件的层次结构。
四、组合模式在Mybatis SqlNode中的应用
4.1 XML映射文件案例
-
动态SQL构建:MyBatis的动态SQL功能通过
<if>
、<choose>
、<when>
、<otherwise>
、<trim>
、<where>
、<set>
、<foreach>
等标签,组合成非常灵活的SQL语句,提高开发人员的效率。 -
SqlNode接口:
SqlNode
接口是MyBatis中用于存储SQL的节点,它有一个apply
抽象方法,用于将SQL节点应用到动态上下文中。
以下是SqlNode
接口及其两个实现类MixedSqlNode
和IfSqlNode
的简单示例:
public interface SqlNode {
boolean apply(DynamicContext context);
}
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
使用案例
假设我们有一个用户表(users
),包含字段id
、name
、email
和status
。我们需要根据不同的条件动态生成查询SQL。
<select id="selectUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
<if test="emails != null and emails.size > 0">
AND email IN
<foreach item="email" collection="emails" open="(" separator="," close=")">
#{email}
</foreach>
</if>
</select>
在这个例子中,<where>
标签内部包含了两个<if>
标签,这两个<if>
标签对应的SqlNode
会被MixedSqlNode
组合在一起进行处理。如果name
和status
不为空,相应的条件会被添加到SQL中。如果emails
列表不为空,<foreach>
标签会生成一个IN条件子句,其中每个邮箱地址都会被包含在列表中。
通过这种方式,MyBatis能够根据传入的参数动态地构建SQL语句,使得SQL语句的构建更加灵活和强大。这种模式的应用提高了MyBatis动态SQL的灵活性和可扩展性。
4.2 Java代码使用案例
1. 定义SqlNode接口和实现类
首先,定义SqlNode
接口和一些实现类,包括TextSqlNode
、IfSqlNode
和ForEachSqlNode
:
public interface SqlNode {
boolean apply(DynamicContext context);
}
public class TextSqlNode implements SqlNode {
private String text;
public TextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendText(text);
return true;
}
}
public class IfSqlNode implements SqlNode {
private String condition;
private SqlNode ifTrue;
public IfSqlNode(String condition, SqlNode ifTrue) {
this.condition = condition;
this.ifTrue = ifTrue;
}
@Override
public boolean apply(DynamicContext context) {
if (Boolean.parseBoolean(context.getBindings().getOrDefault(condition, "false").toString())) {
return ifTrue.apply(context);
}
return false;
}
}
public class ForEachSqlNode implements SqlNode {
private String collection;
private String item;
private String open;
private String close;
private String separator;
private SqlNode contents;
public ForEachSqlNode(String collection, String item, String open, String close, String separator, SqlNode contents) {
this.collection = collection;
this.item = item;
this.open = open;
this.close = close;
this.separator = separator;
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
List<String> emails = (List<String>) context.getBindings().get(collection);
if (emails != null && !emails.isEmpty()) {
context.appendText(open);
for (int i = 0; i < emails.size(); i++) {
context.getBindings().put(item, emails.get(i));
contents.apply(context);
if (i < emails.size() - 1) {
context.appendText(separator);
}
}
context.appendText(close);
}
return true;
}
}
2. 创建DynamicContext类
DynamicContext
类用于存储和传递动态SQL生成过程中的上下文信息:
import java.util.HashMap;
import java.util.Map;
public class DynamicContext {
private StringBuilder sql = new StringBuilder();
private Map<String, Object> bindings = new HashMap<>();
public void appendText(String text) {
sql.append(text);
}
public String getSql() {
return sql.toString();
}
public Map<String, Object> getBindings() {
return bindings;
}
public void setBindings(Map<String, Object> bindings) {
this.bindings = bindings;
}
}
3. 使用SqlNode构建和执行动态SQL
在Java代码中使用SqlNode
构建和执行动态SQL:
public class Main {
public static void main(String[] args) {
DynamicContext context = new DynamicContext();
Map<String, Object> params = new HashMap<>();
params.put("name", "John Doe");
params.put("status", "ACTIVE");
params.put("emails", List.of("john.doe@example.com", "jane.doe@example.com"));
context.setBindings(params);
SqlNode rootNode = new TextSqlNode("SELECT * FROM users WHERE ");
rootNode.apply(context);
new IfSqlNode("name != null",
new TextSqlNode("AND name = #{name}")).apply(context);
new IfSqlNode("status != null",
new TextSqlNode("AND status = #{status}")).apply(context);
new IfSqlNode("emails != null && !emails.isEmpty()",
new ForEachSqlNode("emails", "email", "(", ")", ",",
new TextSqlNode("AND email = #{email}"))).apply(context);
System.out.println("Generated SQL: " + context.getSql());
}
}
在这个示例中,DynamicContext
类用于存储和传递动态SQL生成过程中的上下文信息。Main
类展示了如何使用SqlNode
构建和执行动态SQL。IfSqlNode
和TextSqlNode
用于条件判断和添加文本,而ForEachSqlNode
用于处理集合类型的参数,模拟MyBatis中的<foreach>
标签。
这个示例展示了如何在Java代码中模拟MyBatis的动态SQL行为,包括使用ForEachSqlNode
来处理集合类型的参数。