文章目录
- 一、什么是组合模式
- 二、为什么要使用组合模式
- 三、代码示例
- 代码示例-01(解析组织架构树)
- 代码示例-02(分析网上案例)
- 四、在源码中的应用
- 组合模式在Mybatis框架中的应用
- 五丶在实际开发中的应用
- 六、总结
一、什么是组合模式
官方定义:
组合模式也叫"整体-部分"模式,使用对象的树形结构来表示"整体-部分"的关系.
组合模式的组成元素:
- 抽象组件(接口或者抽象类): 定义抽象方法
- 容器组件(容器节点):实现抽象组件的方法,并且提供一个集合用于保存叶子节点(或者容器节点),然后去循环调用子节点
- 叶子组件(叶子节点):组合模式中的最小执行单元,对抽象组件的方法做具体实现
组合模式分类:
- 安全模式:
从结构图中可以看出,在安全模式中抽象组件只定义公共方法,这样针对于子节点的相关方法就不可以在客户端进行调用(必须使用容器节点才能调用),所以这种做法是安全的,但是缺点就是不能完全针对接口编程,客户端(调用方)必须了解哪些是容器节点,哪些是叶子节点
- 透明模式:
从结构图中可以看出,透明模式是将子类公有的方法都定义在了抽象组件中,这样做的好处就是可以完全针对接口编程,无需关心到底只容器组件还是叶子组件,但是缺点就是叶子节是没有下级节点了,所以那子节点的方法在叶子节点中是无效的,如果不小心误调用了,在编译阶段不会报错,但是在运行阶段就会出现异常(可以通过一定的逻辑处理来规避).
个人理解:
1.组合模式就是专门用来处理树形结构的数据或者带有层级关系结构的数据,可以非常方便的扩展层级结构以及灵活的组合层级结构.
2.组合模式的本质之一就是将[Tree] 转换成[List]或[Map],也可以理解为将层级结构数据转成平级结构数据进行处理
注意: 并不是所有的树形结构都适合使用组合模式.
二、为什么要使用组合模式
首先在实际开发中经常会遇到需要处理树形结构数据的需求,例如<解析组织架构树>或者<解析XML文件中的标签>,在这种需求中通常会使用递归去处理
例如在<解析XML文件中的标签>的需求中,我们往往需要定义逻辑去处理某些标签数据,然而如果后期又新增加了标签的话,很多时候就得去修改原来已经写好的代码,这样就违反了开闭原则,但是如果使用组合模式的话,我们只需要新增对应的叶子节点类去处理新的标签就好了.
所以使用组合模式来处理此类需求的好处,我认为最重要的就是可扩展性.
三、代码示例
代码示例-01(解析组织架构树)
需求:
前端传递Tree结构的数据至后台,后台将Tree转换成List,并且设置每一级的parentId
前端传递过来的数据:
解析之后的结果:
构建数据
public static List<IOrganizationNode> initialNodeData() {
List<IOrganizationNode> list = new ArrayList<>();
List<IOrganizationNode> companyChildList = new ArrayList<>();
List<IOrganizationNode> deptChildList = new ArrayList<>();
List<IOrganizationNode> deptChild01List = new ArrayList<>();
VarOrganizationNode company = new VarOrganizationNode();
company.setName("腾讯科技有限公司");
VarOrganizationNode dept01 = new VarOrganizationNode();
dept01.setName("科技部门");
VarOrganizationNode dept03 = new VarOrganizationNode();
dept03.setName("王者荣耀开发组");
VarOrganizationNode dept05 = new VarOrganizationNode();
dept05.setName("数据处理小组");
VarOrganizationNode dept06 = new VarOrganizationNode();
dept06.setName("建模小组");
deptChild01List.add(dept05);
deptChild01List.add(dept06);
dept03.setContents(deptChild01List);
VarOrganizationNode dept04 = new VarOrganizationNode();
dept04.setName("DNF开发组");
deptChildList.add(dept03);
deptChildList.add(dept04);
dept01.setContents(deptChildList);
VarOrganizationNode dept02 = new VarOrganizationNode();
dept02.setName("运营部门");
companyChildList.add(dept01);
companyChildList.add(dept02);
company.setContents(companyChildList);
list.add(company);
return list;
}
先使用递归实现此需求
这里因为使用到了组合模式中的类,所以强转了一下类(不必关注)
public static void demo01() {
List<Organization> list = new ArrayList<>();
List<IOrganizationNode> data = initialNodeData();
VarOrganizationNode organizationNode = (VarOrganizationNode) data.get(0);
String id = RandomUtil.randomNumbers(5);
Organization.OrganizationBuilder builder = Organization.builder().id(id).name(organizationNode.getName()).parentId("-1");
list.add(builder.build());
recursionChildNode(list, organizationNode.getContents(), id);
log.info("result:{}", JSONUtil.toJsonStr(list));
}
public static void recursionChildNode(List<Organization> list, List<IOrganizationNode> childList, String parentId) {
if (CollUtil.isNotEmpty(childList)) {
for (IOrganizationNode organizationNode : childList) {
VarOrganizationNode item = (VarOrganizationNode) organizationNode;
String id = RandomUtil.randomNumbers(5);
Organization.OrganizationBuilder builder = Organization.builder().id(id).name(item.getName()).parentId(parentId);
list.add(builder.build());
recursionChildNode(list, item.getContents(), id);
}
}
}
再使用组合模式去实现此需求(参考了Mybatis的SqlNode)
案例使用了安全组合模式
//定义组合模式的接口
public interface IOrganizationNode {
void apply(OrganizationContext context);
}
//组合模式中的上下文对象(因为需要方便扩展,不能修改接口上的参数,所以定义了一个上下文对象类)
@Data
public class OrganizationContext {
private List<Organization> organizationList;
private String parentId;
}
//组织架构实体类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Organization {
private String id;
private String parentId;
private String name;
}
//组织架构DTO类
@Data
public class OrganizationDTO {
private String id;
private String name;
private String parentId;
}
//组合模式中的容器对象
public class MixedOrganizationNode implements IOrganizationNode {
private final List<IOrganizationNode> contents;
public MixedOrganizationNode(List<IOrganizationNode> contents) {
this.contents = contents;
}
@Override
public void apply(OrganizationContext context) {
contents.forEach(item -> item.apply(context));
}
}
//组合模式中的叶子节点(组织架构节点,其中包含了下级组织架构集合)
public class VarOrganizationNode extends OrganizationDTO implements IOrganizationNode {
private List<IOrganizationNode> contents;
public List<IOrganizationNode> getContents() {
return contents;
}
public void setContents(List<IOrganizationNode> contents) {
this.contents = contents;
}
@Override
public void apply(OrganizationContext context) {
//从上下文对象中获取List
List<Organization> list = context.getOrganizationList();
//生成当前组织机构ID
String id = RandomUtil.randomNumbers(5);
Organization organization = Organization.builder().id(id).name(this.getName()).parentId(context.getParentId()).build();
//将组织结构添加至集合
list.add(organization);
//获取当前组织机构的下级数据
List<IOrganizationNode> nodeList = this.contents;
//循环下级的组织机构,套娃调用apply()方法
if (CollUtil.isNotEmpty(nodeList)) {
nodeList.forEach(item -> apply(item, id, context));
}
}
//因为需要设置每一级的父级ID,所以需要定义此方法,然后在循环中调用,将上一级的ID设置到上下文对象中
private void apply(IOrganizationNode node, String parentId, OrganizationContext context) {
context.setParentId(parentId);
//这里就是套娃调用的核心代码
node.apply(context);
}
}
//调用代码
public static void apply01() {
List<IOrganizationNode> data = initialNodeData();
log.info("data:{}", JSONUtil.toJsonStr(data));
//初始化上下文对象
OrganizationContext context = new OrganizationContext();
context.setParentId("-1");
context.setOrganizationList(new ArrayList<>());
//使用容器节点去执行组合模式
MixedOrganizationNode mixedNode = new MixedOrganizationNode(data);
mixedNode.apply(context);
log.info("result:{}", JSONUtil.toJsonStr(context.getOrganizationList()));
}
/*
data:
[
{"contents":[{"contents":[{"contents":[{"name":"数据处理小组"},{"name":"建模小组"}],"name":"王者荣耀开发组"},
{"name":"DNF开发组"}],"name":"科技部门"},
{"name":"运营部门"}],"name":"腾讯科技有限公司"}
]
result:
[
{"parentId":"-1","name":"腾讯科技有限公司","id":"42805"},
{"parentId":"42805","name":"科技部门","id":"98035"},
{"parentId":"98035","name":"王者荣耀开发组","id":"30936"},
{"parentId":"30936","name":"数据处理小组","id":"84385"},
{"parentId":"30936","name":"建模小组","id":"26208"},
{"parentId":"98035","name":"DNF开发组","id":"00923"},
{"parentId":"42805","name":"运营部门","id":"46099"}
]
*/
因为网上有大量的通过<公司部门><菜单按钮><文件夹文件>这种结构的案例来讲解组合模式,但是基本没有实际代码实现,要么就是用很简单的输出打印来实现,我认为这种案例即使看完也不一定有什么收获,如果不能用实际开发中的案例来讲解设计模式我认为基本跟Demo差不多,没啥意义,所以我这里使用组合模式来完成一个实际的需求(这个前端传Tree,后端转List,然后保存至数据库的需求我感觉应该挺常见的),代码可能不算完整,大家能明白意思就好.
从这个案例中可以看出来组合模式其实就是一个套娃调用而已,相当于替代了原先的递归代码,如果能理解递归逻辑那么组合模式应该也能很容易理解
组合模式本身是很简单的,我认为重点在于如何使用,例如在这个案例中,我觉得并不需要使用组合模式来完成[Tree Convert List]这么个操作,我相信多数这样的场景下就是抽取一个方法,然后使用递归去处理的(我们项目中就是这样),这里使用组合模式只是为了学习而已.
为什么我觉得类似这样的场景不需要使用组合模式呢
1.使用了组合模式之后明显的发现类变多了,平常一个递归方法就能完成的事情,现在却新增了一系列的类
2.最重要的是没有扩展性,这种[Tree Convert List]操作,明显就没有后续的扩展,也就相当于写完之后基本不会去扩展新的叶子节点
所以说并不是所有的树形结构都适合使用组合模式,树形结构只是组合模式实施的前提条件而已.
代码示例-02(分析网上案例)
//TODO 待完成
四、在源码中的应用
组合模式在Mybatis框架中的应用
首先在Mybatis框架的SqlNode模块(就是解析XML文件,组装Sql语句)中使用到了组合模式
SqlNode-UML图
其中[MixedSqlNode]为容器节点,其他的为叶子节点(对应的就是XML中的各种Sql标签)
下面将选取几个比较简单的叶子节点类说明组合模式的使用(这里只关心组合模式的使用,所以叶子节点中的标签处理逻辑并不过多关注),如果有兴趣的铁渍请自行查看源码
这个类很简单,就是调用者通过这个类去执行组合模式的逻辑而已
package org.apache.ibatis.scripting.xmltags;
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;
}
}
IfSqlNode也很简单,就是判断if条件是否成立,如果成立则继续解析If标签的下级嵌套标签(套娃)
因为if标签下也是可以嵌套别的标签的,所以需要一直向下解析
package org.apache.ibatis.scripting.xmltags;
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final 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;
}
}
StaticTextSqlNode就相当于是最小节点单元,因为在它下面已经没有标签了,所以只需要将Sql语句拼接到上下文对象中就可以了
package org.apache.ibatis.scripting.xmltags;
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
DynamicContext上下对象中维护了一个StringJoiner对象,用作拼接Sql语句
调用方代码(这里只截取一部分)
package org.apache.ibatis.scripting.xmltags;
public class XMLScriptBuilder extends BaseBuilder {
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//见下图
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
//这个方法就是在构造Tree
protected MixedSqlNode parseDynamicTags(XNode node) {
//Tree中的各个叶子节点
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//添加各个叶子节点对象
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
//调用MixedSqlNode的构造方法,并返回
return new MixedSqlNode(contents);
}
}
DynamicSqlSource
package org.apache.ibatis.scripting.xmltags;
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
//初始化上下文对象
DynamicContext context = new DynamicContext(configuration, parameterObject);
//调用MixedSqlNode对象的apply()方法
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//context.getSql()就是拼组好的Sql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
从这里可以看出来Mybatis是先构建好了SqlNode的层级对象(MixedSqlNode),然后调用apply()方法,去逐层解析Sql标签,最终拼接成可执行的Sql语句保存至上下文对象中,供调用方使用.
五丶在实际开发中的应用
1.参考[代码示例-01]
2.我在网上看见有人使用组合模式实现了决策树,有兴趣的铁渍点击此连接去查看
六、总结
可以看出组合模式本身并不难,主要是需要思考运用场景
为什么我认为<组织架构>这种不适合使用组合模式,但是Mybatis的SqlNode是非常适合组合模式的?
因为Mybatis的SqlNode是有具体的叶子节点的[StaticTextSqlNode,IfSqlNode…]
而组织架构是没有具体的叶子节点的,在实际开发中不太会存在[公司节点(CompanyNode),部门节点(DepartmentNode),小组节点(GroupNode)]这种,一般都是通过组织架构类的一个Type字段来区分的
所以组织架构Tree中是不需要扩展的(除非是真的有具体的叶子节点),而Mybatis的SqlNode是存在可扩展性的,比如新增标签(也就是新增叶子节点)
以上仅是我个人观点,相当于抛砖引玉了.
至此组合模式就整理完毕辣~~