- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
组合子模式(Combinator Pattern,又称构图模式)是一种以“组合功能”为中心的函数式编程风格:先定义某个类型T的“原始”构造函数,再用“组合器”把类型T的值按各种方式组合成更复杂的T值。在 java-design-patterns 项目中,该模式被落地为一个可运行的文本行查询引擎:核心Finder接口只提供contains原始查找器,再通过not、or、and三个组合器逐步拼装出advancedFinder、filteredFinder、specializedFinder、expandedFinder等复杂查询器。读完本文,你将理解组合子模式的核心思想与在 Java 8+ 中的具体实现手法,掌握如何用高阶函数与默认方法搭建可复用、可组合的查询 API,并能结合源码与测试复现并验证整个查询流程。
模式概览:目的、定义与适用场景
组合子模式的目的,从 中文版文档 的表述看,是“代表了一种以组合功能为中心的图书馆组织风格”。简单地说:存在某个类型T,有一批用于构造类型T的“原始”值的函数,以及一批可以“以各种方式组合类型T的值以构建更复杂的类型T值”的组合器。
用通俗的话讲:组合器允许从先前定义的“事物”创建新的“事物”。维基百科给出的定义更形式化:组合器是一个高阶函数,仅使用函数应用和之前定义的组合器来定义其参数的结果。这也是计算机科学中“组合逻辑”(combinatory logic)被用作可计算性理论与证明理论简化模型的根源——尽管组合逻辑本身很简单,但它捕获了计算的许多基本特征。
在 英文版文档 中,模式的适用场景被进一步归纳为:
- 问题可以由简单、可复用的组件构建得到;
- 对函数的高模块化与高复用性有明确需求;
- 编程环境支持一等函数与高阶函数(Java 8 起 Lambda 与默认方法即满足该条件)。
类图与执行流程
下图展示了本项目中组合子模式的类结构:Finder作为组合器抽象接口定义了最小契约,Finders提供不同组合逻辑的具体实现,CombinatorApp是该模式的应用载体。
执行层面,组合子遵循“输入 → 基础函数 → 转换 → 与其他逻辑复合 → 最终组合函数 → 输出”的流水线,下图抽象展示了功能叠加与复合的过程:
源码级实现:从原始查找器到组合器
原始查找器Finder.contains
Finder.java 定义了一个函数式接口,其核心抽象方法find(String text)负责在文本中查找行。静态工厂方法contains是唯一的“原始”查找器,它把输入按换行符拆分成若干行,再保留大小写不敏感地包含指定关键词的行:
// 用于查找文本中的行的功能界面。 public interface Finder { // 在文本中查找行的函数。 List<String> find(String text); // 函数 {@link #find(String)} 的简单实现。 static Finder contains(String word) { return txt -> Stream.of(txt.split("\n")) .filter(line -> line.toLowerCase().contains(word.toLowerCase())) .collect(Collectors.toList()); } // 组合器:not。 default Finder not(Finder notFinder) { return txt -> { List<String> res = this.find(txt); res.removeAll(notFinder.find(txt)); return res; }; } // 组合器:or。 default Finder or(Finder orFinder) { return txt -> { List<String> res = this.find(txt); res.addAll(orFinder.find(txt)); return res; }; } // 组合器:and。 default Finder and(Finder andFinder) { return txt -> this .find(txt) .stream() .flatMap(line -> andFinder.find(line).stream()) .collect(Collectors.toList()); } }三个组合器:not、or、and
接口上定义的三个default方法即组合器,它们都以“接收另一个Finder、返回新的Finder”的方式实现组合,返回值仍然是List<String> find(String)的函数,因此可以继续被组合:
not(Finder notFinder):先执行this.find(txt)得到结果列表,再用removeAll(notFinder.find(txt))剔除命中notFinder的行。注意该实现会修改this查找得到的临时结果列表,属于就地求差集。or(Finder orFinder):先执行this.find(txt),再把orFinder命中的行通过addAll追加进来,相当于结果集并集(未做去重)。and(Finder andFinder):语义为“在前一个结果里继续查找”。它对this.find(txt)的结果逐行调用andFinder.find(line),再通过flatMap摊平所有命中行。注意这里andFinder作用在单行字符串上,因此实际匹配的是“行内包含后续关键词”的逐层收敛逻辑。
从源码结构看,not、or两个组合器都是“先求自己的结果,再与对方结果做集合运算”,而and是“以前一步结果作为下一步的输入域”,三种组合方向共同构成了构建复杂查询的基础语法。
由简单查找器拼装复杂查找器:Finders工具类
Finders.java 是一个私有构造函数的工具类,它在contains、not、or、and之上提供了四个开箱即用的复杂查找器:
// 由简单取景器组成的复杂取景器。 public class Finders { private Finders() { } // Finder 用于查找复杂的查询。 public static Finder advancedFinder(String query, String orQuery, String notQuery) { return Finder.contains(query) .or(Finder.contains(orQuery)) .not(Finder.contains(notQuery)); } // 过滤查找器也会查找包含排除查询的查询。 public static Finder filteredFinder(String query, String... excludeQueries) { var finder = Finder.contains(query); for (String q : excludeQueries) { finder = finder.not(Finder.contains(q)); } return finder; } // 专门查询。每个下一个查询都会在上一个结果中查找。 public static Finder specializedFinder(String... queries) { var finder = identMult(); for (String query : queries) { finder = finder.and(Finder.contains(query)); } return finder; } // 扩展查询。寻找替代品。 public static Finder expandedFinder(String... queries) { var finder = identSum(); for (String query : queries) { finder = finder.or(Finder.contains(query)); } return finder; } }四个工厂方法的语义各不相同,适合对照理解“组合方向”:
| 工厂方法 | 签名 | 组合语义 | 单元起始值 |
|---|---|---|---|
advancedFinder | (query, orQuery, notQuery) | 命中query或orQuery,再排除notQuery | contains(query) |
filteredFinder | (query, excludeQueries...) | 命中query后逐一排除多个关键词 | contains(query) |
specializedFinder | (queries...) | 每个下一个查询都在上一个结果中继续查找(交集收敛) | identMult()(返回全部行) |
expandedFinder | (queries...) | 寻找替代品,多个查询结果求并集 | identSum()(返回空列表) |
其中两个私有辅助方法identMult()与identSum()是组合的“单位元”:
private static Finder identMult() { return txt -> Stream.of(txt.split("\n")).collect(Collectors.toList()); } private static Finder identSum() { return txt -> new ArrayList<>(); }从源码实现可以看出:identMult()是and链的乘法单位元(返回全部行,x.and(identMult())不改变语义),identSum()是or链的加法单位元(返回空列表,x.or(identSum())不改变语义)。这也印证了组合子模式“由原始函数 + 组合器构造复杂值”的代数化思想:可变参数场景下,用单位元初始化链式组合,避免在循环中对空列表做特殊处理。
运行与输出:CombinatorApp演示
CombinatorApp.java 是应用入口(在 pom.xml 中被maven-assembly-plugin配置为mainClass),它对一首诗的文本执行四类查询:
var queriesOr = new String[]{"many", "Annabel"}; var finder = Finders.expandedFinder(queriesOr); var res = finder.find(text()); LOGGER.info("the result of expanded(or) query[{}] is {}", queriesOr, res); var queriesAnd = new String[]{"Annabel", "my"}; finder = Finders.specializedFinder(queriesAnd); res = finder.find(text()); LOGGER.info("the result of specialized(and) query[{}] is {}", queriesAnd, res); finder = Finders.advancedFinder("it was", "kingdom", "sea"); res = finder.find(text()); LOGGER.info("the result of advanced query is {}", res); res = Finders.filteredFinder(" was ", "many", "child").find(text()); LOGGER.info("the result of filtered query is {}", res);其中text()返回如下文本块(含大小写混排,用于验证匹配的大小写不敏感性):
private static String text() { return "It was many and many a year ago,\n" + "In a kingdom by the sea,\n" + "That a maiden there lived whom you may know\n" + "By the name of ANNABEL LEE;\n" + "And this maiden she lived with no other thought\n" + "Than to love and be loved by me.\n" + "I was a child and she was a child,\n" + "In this kingdom by the sea;\n" + "But we loved with a love that was more than love-\n" + "I and my Annabel Lee;\n" + "With a love that the winged seraphs of heaven\n" + "Coveted her and me."; }程序输出如下(日志前缀因环境而异):
the result of expanded(or) query[[many, Annabel]] is [It was many and many a year ago,, By the name of ANNABEL LEE;, I and my Annabel Lee;] the result of specialized(and) query[[Annabel, my]] is [I and my Annabel Lee;] the result of advanced query is [It was many and many a year ago,] the result of filtered query is [But we loved with a love that was more than love-]对照结果可以验证各组合器的行为:
- expanded(or):
"many"与"Annabel"的命中行取并集,得到 3 行(注意第 1 行同时命中两个关键词,由于or不做去重,该行只出现一次,因为contains("many")命中的行集合与contains("Annabel")命中的行集合交集只有一行,addAll后 2+2-1 去重前为 4 个元素,实际输出为 3 行表明两集合仅重叠 1 行;如需严格去重,可在此基础上增加distinct(),本项目实现未做去重); - specialized(and):先找到包含
"Annabel"的行,再在其中找包含"my"的行,最终收敛到I and my Annabel Lee;; - advanced:
contains("it was")与contains("kingdom")求并后排除contains("sea"),剩下第一行; - filtered:命中
" was "后排除"many"与"child",得到唯一行。
测试验证:组合器行为被单测固化的证据
项目的测试代码对上述行为做了断言级别的固化,可作为“行为即规范”的证据:
- FinderTest.java 验证
contains:在包含两行文本的样例中,Finder.contains("second").find(example)只返回命中行"the second one "。 - FindersTest.java 分别断言四个工厂方法的输出:
advancedFinder("it was", "kingdom", "sea")返回 1 行,即"It was many and many a year ago,";filteredFinder(" was ", "many", "child")返回 1 行,即"But we loved with a love that was more than love-";specializedFinder("love", "heaven")返回 1 行,即"With a love that the winged seraphs of heaven";expandedFinder("It was", "kingdom")返回 3 行。
- CombinatorAppTest.java 断言
CombinatorApp.main执行不抛异常。
这些测试同时展示了一个关键用法:specializedFinder等组合器可以脱离CombinatorApp的固定文本独立使用,说明该 API 本身是松耦合、可复用的。
组合子模式的好处与权衡
好处
从开发者视角看,组合子模式带来的收益在 中文版文档 与 英文版文档 中被总结为:
- API 由领域术语组成:查询接口直接以
contains、not、or、and等词汇呈现,调用方表达的是业务意图而非底层遍历逻辑; - 组合阶段与应用阶段分离:先构造一个
Finder实例,之后再执行它,符合“先声明后执行”的函数式风格; - 适用于并行环境:因为组合器返回的是不可变描述性的函数对象,组合与执行解耦后,同一组合物可在多个线程/输入上重复执行;
- 模块化与复用性:复杂任务被拆解为简单可组合函数,代码以声明式风格编写,可读性与可维护性更好;
- 通过函数组合还可天然支持惰性求值(
find在被调用前不会执行任何实际计算)。
权衡
需要理性看待的代价(英文版文档 明确列出):
- 学习曲线陡峭:对不熟悉函数式编程的开发者,理解“组合器返回函数而非结果”需要一定时间;
- 中间对象开销:
not/or会在每次组合时创建中间列表,and使用flatMap产生中间流,组合链越长中间分配越多; - 调试困难:函数组合的抽象层次高,调用栈中难以直观定位“哪一步组合引入了问题”。
针对第二点,本项目实现也给出了两个可优化方向(属于对现有实现的推断):or的addAll不保证去重、not的removeAll依赖List的equals语义,若行内出现重复文本,结果集会与“集合论意义”上的并/差集有偏差;实际生产可改用LinkedHashSet或.distinct()收敛后再聚合。
现实世界的例子与相关模式
现实世界的组合器
- JDK 自带的高阶函数组合:
java.util.function.Function#compose与java.util.function.Function#andThen,前者将两个Function先右后左组合,后者先左后右组合,是语言层面最典型的组合器应用; - 函数式语言(如 Haskell、Scala)广泛使用组合器进行解析器构建与 UI 构造;
- 解析表达式文法(PEG)等 DSL 解析器,以及 JavaScript、Python、Ruby 的函数式编程库,都以组合器为组织范式。
相关模式对比
在 英文版文档 中,与本模式对照的相关模式包括:
- 责任链模式:同样通过“链式”组织处理单元,但责任链组合的是对象实例(沿链传递请求),组合子组合的是函数(在构造期完成拼装);
- 装饰器模式:都通过包装增强功能,但装饰器聚焦于对象增强,组合子聚焦于函数复合;
- 策略模式:两者都在运行时选择算法,但策略用对象切换实现,组合子用函数组合实现。
如何查看与运行本项目
仓库为只读状态,以下操作仅用于本地查看、构建与运行:
# 查看组合子模块源码与测试 # 源码:combinator/src/main/java/com/iluwatar/combinator/ # 测试:combinator/src/test/java/com/iluwatar/combinator/ # 在仓库根目录执行 Maven 构建并运行该模块 ./mvnw -pl combinator test # 直接运行演示应用(需要已构建 classpath) ./mvnw -pl combinator exec:java -Dexec.mainClass=com.iluwatar.combinator.CombinatorApp该模块依赖 slf4j-api 与 logback-classic(见 pom.xml),日志输出即上文展示的四条查询结果。如需快速体验,也可以直接阅读 CombinatorApp.java 中的main方法并自行修改查询参数。
小结
组合子模式在本项目中的落地路径非常清晰:一个原始查找器contains+ 三个组合器not/or/and→ 工具类Finders拼装出四个复杂查找器 → 应用CombinatorApp演示并输出 → 测试类固化行为。整套实现只用了约 200 行 Java 代码,却完整展示了“以组合功能为中心组织库”的函数式思想。当你面对“由简单可复用组件构建复杂行为”的问题,且运行环境支持一等函数时,组合子模式是兼顾表达力、复用性与可测性的实用选择——正如本项目所展示的,它甚至可以让文本查询这类需求变得像搭积木一样自然。
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
java-design-patterns 组合子模式(Combinator Pattern)实战:用函数组合构建可复用的文本查询 DSL
java design patterns 组合子模式(Combinator Pattern)实战:用函数组合构建可复用的文本查询 DSL 组合子模式(Combi
示例工程教程java-design-patterns 项目中的 Combinator 模式:用函数组合构建灵活可复用的 Java 查询组件
java design patterns 项目中的 Combinator 模式:用函数组合构建灵活可复用的 Java 查询组件 Combinator(组合子)模
示例工程教程Java 组合模式(Composite Pattern)实战指南:用 java-design-patterns 构建灵活的树形结构
Java 组合模式(Composite Pattern)实战指南:用 java design patterns 构建灵活的树形结构 组合模式(Composite
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考