如何彻底搞懂组合(Composite)设计模式?

当我们在设计系统对象关系时,有时候会碰到这样一种场景,一个对象中包含了另一组对象,两者构成一种”部分-整体”的关联关系。


正如上图中所展示的,当我们面对这样一种对象关系时,通常都需要分别构建单独的访问方式,一种面向单个对象,另一种则面向组合对象。显然,这样的实现方法存在一些缺陷,如果从部分到整体的关联关系发生了变化,那么我们可能需要同时调整这两种访问方式,从而对系统的代码结构造成比较大的影响。那么,有没有更好的处理方式呢?今天要介绍的组合设计模式就是专门用来应对这种场景的。

组合设计模式的概念和简单示例

简单来讲,组合设计模式的核心思想是只提供一种访问方式,这种访问方式可以同时用来完成对上图中单个对象和组合对象的有效处理。在具体实现方法上,组合模式将代表“部分”组织结构和“整体”组织结构的所有对象都组合到一种树形结构中,其基本的组成结构如下所示。


可以看到,在组合模式中首先存在一个Component接口,所有的对象都必须实现这个接口。同时,该接口也面向Client暴露了统一访问入口。因此,Client只需要实现一种访问方式即可。然后,这里的Leaf代表“部分”,而“Component”代表“整体”,它们都实现了Component接口。而Composite中还管理了一组子Component,所以具备一种树状结构。

明白了组合模式的基本结构,接下来我们来给出对应的案例代码。我们知道任何一个语句(Sentence)都有单词(Word)组成,而单词又由字母(Letter)组成。这三者之间就是一种典型的”部分-整体”的关联关系。


现在,让我们来定义一个组合类,我们直接将该类命名为Composite。

public abstract class Composite {

  private final List<Composite> children = new ArrayList<>();

  public void add(Composite composite) {

    children.add(composite);

  }

  public int count() {

    return children.size();

  }

  protected void printThisBefore() {

  }

  protected void printThisAfter() {

  }

  public void print() {

    printThisBefore();

    children.forEach(Composite::print);

    printThisAfter();

  }

}

有了Composite类,接下来就可以构建各种子Composite类,例如如下所示的代表字母的Letter类。

public class Letter extends Composite {

  private final char character;

  public Letter(char character) {

super();

this.character = character;

  }

  @Override

  protected void printThisBefore() {

    System.out.print(character);

  }

}

在Letter类的基础上,我们可以进一步构建代表单词的Word类。

public class Word extends Composite {

  public Word(List<Letter> letters) {

    letters.forEach(this::add);

  }

  public Word(char... letters) {

    for (char letter : letters) {

      this.add(new Letter(letter));

    }

  }

  @Override

  protected void printThisBefore() {

    System.out.print(" ");

  }

}

基于Word类,代表语句的Sentence类实现也非常简单。

public class Sentence extends Composite {

  public Sentence(List<Word> words) {

    words.forEach(this::add);

  }

  @Override

  protected void printThisAfter() {

    System.out.print(".\n");

  }

}

最后,我们可以通过如下方式构建一个Sentence,并调用它的print方法来实现对语句的打印。

var words = List.of(

        new Word('H', 'e', 'l', 'l', 'o'),

        new Word('G', 'e', 'e', 'k', 'e'),

        new Word('T', 'i', 'm', 'e')

);

Sentence sentence  = new Sentence(words);

Sentence.print();

上述代码的执行结果就是下面这句话。

Hello Geek Time

组合设计模式在Mybatis中的应用

接下来,我们来分析组合设计模式的在主流开源框架中的应用。相信使用过Mybatis的同学都知道我们可以使用它所提供的各种标签对SQL语句进行动态组合,这些标签常见的包含if、choose、when、otherwise、trim、where、set、foreach等。虽然并不建议在SQL级别添加过于复杂的逻辑判断,但面对一些特定场景时,Mybatis的动态SQL机制确实能够提高开发效率。

一个采用Mybatis动态SQL的示例如下所示,可以看到这里使用了<where>和<if>这两个标签,同时在<if>标签中使用test断言进行非空校验。

<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的解析,显然,解析过程并不简单。如果对每个标签都进行硬编码处理,那边处理流程和代码逻辑会很混乱且不易维护。为此,Mybatis就引入了组合设计模式。针对前面所展示的各个动态SQL节点配置,Mybatis专门设计了一个SqlNode接口,通过这个接口我们可以构建一种树形结构,该接口定义如下。

public interface SqlNode {

  boolean apply(DynamicContext context);

}

可以看到,在SqlNode接口中只定义了一个apply方法,而该方法中传入的是一个DynamicContext对象。从命名上讲,DynamicContext代表一种动态上下文组件,保存着所有动态SQL的解析结果。当apply方法被调用时,它会根据该SQLNode中所持有的动态SQL节点配置信息进行递归解析。当这些动态SQL节点被解析完毕之后,我们就可以从DynamicContext中获取一条动态生成的目标SQL语句。

作为抽象组件,SqlNode拥有丰富的类层结构。


上图中,我们先来看一下MixedSqlNode类的代码。

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;

  }

}

可以看到,在MixedSqlNode中保存着一个SqlNode列表,所以它是一种Composite组件。在MixedSqlNode的apply方法中,通过一个for循环对SqlNode列表中的所有节点信息进行遍历,并依次调用它们的apply方法。显然,这是一种递归操作。

然后我们再来找一个典型的SqlNode实现,这里选择IfSqlNode。

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;

  }

}

IfSqlNode的作用就是解析动态SQL节点中的<if>标签,这里用到了一个工具类ExpressionEvaluator,通过该类的evaluateBoolean方法来对配置节点中的test表达式进行评估。如果test表达式返回的为true,那么就执行子节点的apply方法,完成对动态SQL的填充。

Mybatis中的其他SqlNode子类的结构与IfSqlNode类似,结合日常的使用方法,各自功能也比较明确。最后我们来看一下DynamicSqlSource类,在组合设计模式中,该类扮演了客户端的角色。

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

    rootSqlNode.apply(context);

}

}

可以看到,DynamicSqlSource依赖于SqlNode,通过构建DynamicContext并应用SqlNode的apply方法完成动态SQL的解析过程。

关于Mybatis中组合设计模式的应用就介绍完了,我们来做一个总结。我们看到SqlNode 接口有多个实现类,每个实现类用来处理对应的一个动态SQL节点。结合组合设计模式的基本结构图,可以认为 SqlNode 相当于是Component接口,MixedSqlNode 相当于是Composite组件,而其它SqlNode的子类则是Leaf组件,最后DynamicSqlSource则是整个模式的Client。整个组合模式的类结构如下图所示。



对于系统中具有“部分-整体”结构的场景而言,组合模式能够帮助我们构建优雅的递归操作。现实中有很多对象之间的复杂关联关系都可以通过组合模式来进行简化,从而实现统一的对象访问方式。

组合模式在主流的开源框架中应用也非常广泛,例如Mybatis在解析动态SQL语句时,就用到了组合模式来解析树状的SQL节点。在今天的内容中,我们对组合模式的基本概念以及在Mybatis中的应用方式进行了详细的展开。

实现组合模式的前提是需要我们合理梳理对象之间存在的“部分-整体”关联关系,有时候这种关联关系可能有很多层,所以表现为是一种递归结构。一旦构建了符合组合模式的代码框架结构,那么通过构建各种子Composite类,我们就可以为系统添加丰富的新功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/649313.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据挖掘案例-航空公司客户价值分析

文章目录 1. 案例背景2. 分析方法与过程2.1 分析流程步骤2.2 分析过程1. 数据探索分析2. 描述性统计分析3. 分布分析1.客户基本信息分布分析2. 客户乘机信息分布分析3. 客户积分信息分布分析 4. 相关性分析 3. 数据预处理3.1 数据清洗3.2 属性约束3. 3 数据转换 4. 模型构建4. …

【面经】单片机

1、单片机IO口工作方式 输入 模拟输入&#xff08;GPIO_Mode_AIN&#xff09;&#xff1a;关闭施密特触发器&#xff0c;将电压信号传送到片上外设模块&#xff0c;通常用于连接模拟信号源。浮空输入&#xff08;GPIO_Mode_IN_FLOATING&#xff09;&#xff1a;在浮空输入状态…

回收站清空的文件怎么恢复?8个方法公开(2024更新版)

“我太粗心了&#xff0c;刚想恢复部分回收站中误删的重要文件&#xff0c;一不小心把回收站清空了&#xff0c;现在还有什么方法可以恢复它们吗&#xff1f;” 在数字时代&#xff0c;电脑已经成为我们日常生活和工作中不可或缺的工具。然而&#xff0c;随着我们对电脑的依赖加…

etcd 和 MongoDB 的混沌(故障注入)测试方法

最近在对一些自建的数据库 driver/client 基础库的健壮性做混沌&#xff08;故障&#xff09;测试, 去验证了解业务的故障处理机制和恢复时长. 主要涉及到了 MongoDB 和 etcd 这两个基础组件. 本文会介绍下相关的测试方法. MongoDB 中的故障测试 MongoDB 是比较世界上热门的文…

【算法】排序——加更

补充1个排序&#xff1a;希尔排序 思路&#xff1a;首先定义一个gap,从第0个数开始&#xff0c;每隔一个gap取出一个数&#xff0c;将取出来的数进行比较&#xff0c;方法类似插入排序。第二轮从第二个数开始&#xff0c;每隔一个gap取出一个数再进行插入排序。四轮就可以取完…

新手一次过软考高级(系统规划与管理师)秘笈,请收藏!

2024上软考已经圆满结束&#xff0c;距离下半年的考试也只剩下半年不到的时间。需要备考下半年软考高级的小伙伴们可以抓紧开始准备了&#xff0c;毕竟高级科目的难度可是不低的。 今天给大家整理了——系统规划与管理师的备考资料 &#xff0c;都是核心重点&#xff0c;有PDF&…

微博v14.5.1,集成猪手模块2.3.0-276,移除广告和各类推广提示

软件介绍 微博 v14.5.1&#xff0c;内置猪手模块直装版是一款专业优化的微消客户端&#xff0c;该软件融合了咸猪手模块&#xff0c;并提供了用户友好的自定义选项。这些选项包括广告移除、停止推荐内容、消除各类提示消息等功能&#xff0c;旨在提升用户的个性化使用体验。 …

最详细Linux提权总结(建议收藏)

1、内核漏洞脏牛提权 查看内核版本信息 uname -a 具体提权 1、信息收集配合kali提权 uname -a #查看内核版本信息 内核版本为3.2.78&#xff0c;那我们可以搜索该版本漏洞 searchsploit linux 3.2.78 找到几个可以使用的脏牛提权脚本&#xff0c;这里我使用的是40839.c脚…

Facebook广告如何开户以及投放费用?

Facebook作为全球最大的社交媒体平台之一&#xff0c;成为了企业与个人推广品牌、产品或服务的重要渠道。其精准的广告定向功能和庞大的用户基数&#xff0c;为广告主提供了无限的商机。云衔科技为企业提供专业的Facebook上开户和运营服务&#xff0c;助力您高效获客。 一、Fa…

【Spring Cloud】Feign整合服务容错中间件Sentinel

文章目录 引入sentinel依赖配置文件为被容错的接口指定容错类创建容错类修改controller演示扩展为被容错的接口更改容错类创建回退工厂类演示 总结 上一篇文章中我们已经对服务容错中间件 Sentinel 持久化的两种模式进行了全面解析&#xff0c;本文我们将对Feign和Sentinel进行…

学术图表的基本配色方法

不论是商业图表还是专业图表&#xff0c;图表的配色都极其关键。图表配色主要有彩色和黑白两种配色方案。刘万祥老师曾提出&#xff1a; “在我看来&#xff0c;普通图表与专业图表的差别&#xff0c;很大程度就体现在颜色运用上。” 对于科学图表&#xff0c;大部分国内的期…

lua 计算第几周

需求 计算当前赛季的开始和结束日期&#xff0c;2024年1月1日周一是第1周的开始&#xff0c;每两周是一个赛季。 lua代码 没有处理时区问题 local const 24 * 60 * 60 --一整天的时间戳 local server_time 1716595200--todo:修改服务器时间 local date os.date("*t…

利用阅读APP3.0目录展示要查看的内容02

要实现前面提到的功能并不困难&#xff0c;只要导入如下规则即可: 打开APP导入对应规则: 导入后的目录规则界面: 导入后的替换规则界面: 规则文件详细内容: 1. 目录规则&#xff1a; 2. 替换规则 除了直接导入上述文件&#xff0c;也可以自己添加规则。总之&#xff0c;就是利用…

蓝桥杯第十四届国赛B组刷题笔记

A-0子2023&#xff1a; 题目&#xff1a; 小蓝在黑板上连续写下从 11 到 20232023 之间所有的整数&#xff0c;得到了一个数字序列&#xff1a; &#x1d446;12345678910111213...20222023S12345678910111213...20222023。 小蓝想知道 &#x1d446;S 中有多少种子序列恰好等…

夏日将至,给手机装个“液冷”降温可行吗?

夏天出门在外&#xff0c;手机总是更容易发热&#xff0c;尤其是顶着大太阳用手机的时候&#xff0c;更是考验手机的散热能力。如果你也是一个对手机体验有追求的人&#xff0c;比较在意手机的温度&#xff0c;那么可以考虑入手一个微泵液冷手机壳。 【什么是微泵液冷壳&#…

《浪姐》也搞live直播,真成综艺流量密码了?

继《歌手》之后&#xff0c;芒果的另一档综艺《浪姐》也将开启直播。 《乘风2024》官博宣布进行突击加场直播赛&#xff0c;姐姐们将面临全开麦live直播&#xff0c;摇人投票排在前十的姐姐获得live直播抢先权。 这是看《歌手2024》直播赛制火了&#xff0c;也想蹭个热度搞直…

JavaScript 新特性:新增声明命令与解构赋值的强大功能

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;JavaScript 精粹 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; ES5、ES6介绍 文章目录 &#x1f4af;声明命令 let、const&#x1f35f;1 let声明符&a…

豆包模型最新数据评测!性能究竟如何?

豆包模型最新数据评测&#xff01;性能究竟如何&#xff1f; 前言 就在5月27日&#xff0c;字节跳动旗下的豆包大模型在火山引擎原动力大会上正式发布&#xff0c;本次大会中豆包的模型能力也引发行业关注。 介绍豆包 豆包是一个多功能 AI 助手&#xff0c;为你的生活、学习、工…

什么是独特摆动交易策略?fpmarkets1分钟讲清楚

摆动交易策略想必各位投资者都已经接触过了&#xff0c;但是什么是独特摆动交易策略&#xff1f;各位投资者知道吗&#xff1f;其实很简单&#xff0c;这是一种基于斐波纳契工具的独特摆动交易策略。下面fpmarkets1分钟讲清楚&#xff0c;趋势总会经历调整&#xff0c;而这些调…

生产者发送源码

具体流程 Producer先从本地尝试获取路由信息本地无缓存的路由信息时&#xff0c;从注册中心中获取路由信息&#xff0c;并缓存到本地获取到的路由信息包含了Topic下的所有Queue&#xff0c;Producer就可以采取负载均衡策略把消息发送到某个队列里Producer发送消息到Broker成功…