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

news2024/9/25 7:18:33

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


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

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

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


可以看到,在组合模式中首先存在一个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类,我们就可以为系统添加丰富的新功能。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

11.Redis之zset类型

1.zset类型基本介绍 有序描述的是&#xff1a;升序/降序 Set 集合 1.唯一 2. 无序 孙行者,行者孙, 者行孙 >同一只猴~~ List有序的 孙行者,行者孙, 者行孙 >不同的猴~~ zset 中的 member 仍然要求是唯一的!!(score 则可以重复) 排序的规则是啥? 给 zset 中的 member 同…

太狠了,凌晨5点面试。。

(关注数据结构和算法&#xff0c;了解更多新知识) 网上看到一网友发文说收到面试邀请&#xff0c;面试时间竟然是早晨5点&#xff0c;这是要猝死的节奏。有的网友说应该是下午 5 点&#xff0c;如果是下午 5 点直接写下午 5 点就行了&#xff0c;或者写 17 点也行&#xff0c;直…

中医理疗元宇宙 科技赋能中医药产业走向国际市场

基于380亿参数量&#xff0c;对中医药海量文本进行数据训练&#xff0c;实现方剂优化、机制阐释和新适应症的精准发现……日前在天津召开的数智赋能大健康产业新质生产力暨第四届中医药国际发展大会上&#xff0c;由天士力医药集团与华为云共同开发的“数智本草”中医药大模型正…

【再探】设计模式—职责链模式、命令模式及迭代器模式

行为型设计模式研究系统在运行时对象之间的交互&#xff0c;进一步明确对象的职责。有职责链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式及访问模式共11种。 1 职责链模式 需求&#xff1a;1) 请求能被多…

自动驾驶中的“ImageNet”?CVPR2024新作OccFeat:BEV 自监督预训练

论文标题&#xff1a; OccFeat: Self-supervised Occupancy Feature Prediction for Pretraining BEV Segmentation Networks 论文作者&#xff1a; Sophia Sirko-Galouchenko, Alexandre Boulch, Spyros Gidaris, Andrei Bursuc, Antonin Vobecky, Patrick Prez, Renaud Ma…

Nginx实战(安装部署、常用命令、反向代理、负载均衡、动静分离)

文章目录 1. nginx安装部署1.1 windows安装包1.2 linux-源码编译1.3 linux-docker安装 2. nginx介绍2.1 简介2.2 常用命令2.3 nginx运行原理2.3.1 mater和worker2.3.3 Nginx 的工作原理 2.4 nginx的基本配置文件2.4.1 location指令说明 3. nginx案例3.1 nginx-反向代理案例013.…

紧固件松动的危害及原因——SunTorque智能扭矩系统

智能扭矩系统-智能拧紧系统-扭矩自动控制系统-SunTorque 紧固件松动&#xff0c;这一看似微小的机械问题&#xff0c;实际上可能引发一系列严重的后果。在机械设备中&#xff0c;紧固件扮演着至关重要的角色&#xff0c;它们通过紧固作用将各个部件紧密连接在一起&#xff0c;…

招聘视角,看数据产品经理求职面试技巧

近几年负责数据产品团队&#xff0c;经历团队人员的变迁&#xff0c;进行过几百简历的筛选&#xff0c;近百场社招、校招面试。金三银四的求职/招聘季接近尾声&#xff0c;想把自己招聘数据产品经理的过程进行总结&#xff0c;分享给想找数据产品经理工作的求职者。 一、数据产…

【C语言】数据指针地址的取值、赋值、自增操作避坑

【C语言】数据指针的取值、赋值、自增操作避坑 文章目录 指针地址指针自增指针取值、赋值附录&#xff1a;压缩字符串、大小端格式转换压缩字符串浮点数压缩Packed-ASCII字符串 大小端转换什么是大端和小端数据传输中的大小端总结大小端转换函数 指针地址 请看下列代码&#…

css3实现0.5px边框

效果图 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>css3实现0.5px边框</title><s…

HotSpot虚拟机的几个实现细节

文章目录 STW安全点安全区域记忆集与卡表读写屏障 STW 收集器在根节点枚举这步都是必须要暂停用户线程的&#xff08; STW &#xff09;&#xff0c;如果不这样的话在根节点枚举的过程中由于引用关系在不断变化&#xff0c;分析的结果就不准确 安全点 收集器在工作的时候某些…

如何设置XHSC(华大)单片机的IO口中断

XHSC(华大)单片机IO口中断使用 一、代码说明 华大单片机的历程在华大或者小华的官网上都可以下载到,但是我们下载的历程基本注释都是非常简单,有的还没有注释;再加上小华跟华大的历程在代码架构上有所区别,所以新手在直接调用华大或者小华历程后,历程代码的可读性并不…

解析气膜场馆造价—轻空间

随着社会的发展和对环保及时间成本的重视&#xff0c;气膜场馆逐渐成为众多体育场馆的首选建筑模式。气膜建筑包括气膜篮球场、气膜室内足球场、气膜羽毛球场、气膜乒乓球馆、气膜网球场以及气膜滑冰场等&#xff0c;因其多项优势受到广泛应用。 气膜场馆的显著特点 1. 气膜场馆…

不可错过的数据存储指南:JVS物联网平台存储策略详解

在物联网时代&#xff0c;数据的采集、存储和分析成为了关键环节。随着设备点位不断生成大量数据&#xff0c;如何高效地管理和保存这些数据&#xff0c;同时考虑存储成本和后续的数据分析价值&#xff0c;成为了亟待解决的问题。JVS物联网平台提供了灵活多样的存储策略&#x…

安泰电子:使用高压放大器时有哪些需要注意的呢

随着科技的不断进步&#xff0c;高压放大器在各种科学实验、工程应用和产业生产中扮演着重要的角色。然而&#xff0c;由于高压放大器的特殊性&#xff0c;使用时需要特别小心和谨慎。下面将详细介绍使用高压放大器时需要注意的事项&#xff0c;以确保安全、稳定地进行实验和应…

搭载昇腾310NPU的Orange Pi AIpro开箱体验以及深度学习样例测试

Orange Pi AIpro开箱体验以及样例测试 随着人工智能和物联网技术的快速发展&#xff0c;单板计算机&#xff08;Single Board Computer, SBC&#xff09;在创客和开发者社区中越来越受到欢迎。我最近入手了一款高性能的单板计算机——Orange Pi AIpro。 在入手此款AI开发板之…

【BI 可视化插件】怎么做? 手把手教你实现

背景 对于现在的用户来说&#xff0c;插件已经成为一个熟悉的概念。无论是在使用软件、 IDE 还是浏览器时&#xff0c;插件都是为了在原有产品基础上提供更多更便利的操作。在 BI 领域&#xff0c;图表的丰富性和对接各种场景的自定义是最吸引人的特点。虽然市面上现有的 BI 软…

如何理解 Java 类和对象

Java 中的类和对象是学习 Java 编程的基础之一。类是 Java 中的核心概念之一&#xff0c;它提供了一种组织和封装数据以及相关行为的方式。对象是类的实例&#xff0c;它是在运行时创建的&#xff0c;具有特定的状态和行为。 类和对象的概念 1. 类&#xff08;Class&#xff…

Micro SD封装是什么?

我们了解客户对于Micro SD封装的疑问。在这篇文章中&#xff0c;我们将详细解释Micro SD封装是什么&#xff0c;以及其在存储领域的技术原理和应用情况&#xff0c;帮助客户更好地理解这一技术。 1. Micro SD封装的定义 Micro SD封装是指一种特定尺寸的存储芯片封装方式&#x…

汇凯金业:纸黄金和实物黄金的价格有什么区别

纸黄金和实物黄金的价格主要受到全球黄金市场行情的影响&#xff0c;二者的基础价格并无太大差异&#xff0c;但在具体交易时&#xff0c;可能会存在一些价格上的区别&#xff0c;这些差异主要来自以下几个方面&#xff1a; 交易费用与管理费&#xff1a;纸黄金交易通常需要支…