Reduce Functions
封面来源:由博主个人绘制,如需使用请联系博主。
本文涉及的代码:mofan212/reduce-functions
0. 前言
0.1 我的近况
最近一两个月甚是怠惰,自从购入新电脑后,每周除了宅着打游戏就还是打游戏,在嚎哭深渊里刻苦研究雪球的运动轨迹,最终也没得出什么结论,六、七月里新博文的更新屈指可数。
言归正传,最近一个月来自己只做了两个任务项,但为实现这两个功能竟然用了将近 4000 行代码,以我看来,能够出现如此庞大的编码量是因为前期的不合理设计,前期的设计完全不能沿用到本次任务项的实现中,最终只能重写处理逻辑,造成如此庞大的工作量。
反省结束。
本次的两个任务项并不复杂,但切合我司的业务来谈,繁琐却也是真的。个人认为完成得较为出色,但自己也能隐约感到在接口设计方面还有不足,但具体到详细点自己也答不上,这应该是自己技术力不够导致的,半桶水的水平下,对不合理点有感知,但又列不出具体。
虽说反省结束,但在不知不觉中又开始了反省。为了能够更好地反省,决定谈谈其中的接口设计,以及部分实现细节,相比于实际业务肯定会得到简化,但也大差不差。
与其说本文的标题是《Reduce-Functions》,倒不如是《我的反省》?🤣
0.2 给你讲讲需求
给出一个 JSON 数组,定义数组中的每一项都是一个“节点”,这些节点有不同的类型,并且拥有不同的内部结构。
给出几个 JSON 对象,定义这些 JSON 对象为“组件”,已知每个节点可能包含若干个组件。
现需要 根据给定的组件标识,在每个节点中定位这些组件,以便后续对这一类组件进行其他操作。
比如:
1 | [ |
其中 nodeId
和 nodeType
是节点的附加信息,而 componentA
和 componentB
显然就是两个不同的组件。
1. 需求分析与实现思路
1.1 组件的划分
一个节点包含若干个组件,组件也可以划分为很多种。
顶层组件
规定 顶层组件 是能够在节点中直接通过一个 key 就能被定位到的组件,比如:
1 | { |
其中的 componentA
和 componentB
指向的 JSON 对象就是顶层组件。
为了描述的简洁性,后续将直接使用 key 指代其对应的 JSON 对象。
普通组件与子组件
在某些情况下,一个顶层组件也可以由若干个组件组成,比如:
1 | { |
其中的 componentC
是一个顶层组件,它由 componentA
、componentB
和 componentD
组成,前两个组件还是顶层组件,而 componentD
是一个普通组件(或者说非顶层组件)。
针对 componentC
,还可以说 componentA
、componentB
和 componentD
是 componentC
的子组件。
每个组件都可以有子组件,这些子组件可以是顶层组件,也可以是普通组件。 这里的“每个组件”也包括子组件,也就是说子组件也可以有子组件(或者说孙组件?这不重要)。
列表组件
多个同种类型组件可以聚合为一个列表组件。
对列表组件来说,它的子组件就是列表中的每一项相同类型的组件。
列表组件既可以作为顶层组件,也可以作为普通组件。
1 | { |
其中的 componentDs
就是一个列表组件,它也还是一个顶层组件,它由 componentD
聚合而成。
1 | { |
其中的 componentEs
也是一个列表组件,但它只是一个普通组件,是顶层组件 componentF
的子组件。
1.2 从节点角度
每个节点有不同的类型,可以通过 nodeType
对节点进行分类,如果存在相同的节点,则使用 nodeId
进行区分(nodeId
在本文中无需关心,实现中并不会用到)。
对节点来说,它由若干个顶层组件组成,顶层组件可能又由若干个普通组件聚合而成。
给出若干个节点,需要找出这些节点中的目标组件,从节点角度出发,一般思路是:
- 首先由节点定位到顶层组件;
- 然后通过顶层组件定位到子组件;
- 不断深度搜索与回溯,直到找到所有目标组件。
这样会有一个问题,假设组件数量固定,节点又可以由任意个顶层组件组成,可以认为在顶层组件数量确定的情况下,可以组成无数个不同类型的节点。这样的话,如果从节点角度出发,后续每增加一种节点,岂不是就要增加一种节点处理类?
1.3 从组件角度
一个节点由若干个顶层组件构成,不同类型的节点可能存在相同的顶层组件。
组件的组合(包括顶层组件)可以形成另一个组件,也就是说一个组件可以有若干个依赖项(即多个子组件)。
顶层组件也是组件,“顶层”是指能够从节点角度出发,通过一个 key 就定位到目标组件,而不是不能再组合的意思。
针对任意组件来说,可以在定位到其父级组件后,通过一个 key 定位到当前组件。 对于顶层组件,可以认为其父级组件就是节点。
通过这样的节点依赖关系,可以绘制出节点依赖图。如果需要定位目标组件,那么就是在依赖图中找到到目标组件的所有路径。 这个路径就是定位路径,已知的是,这些路径的第一个节点一定是顶层组件,最后一个节点是目标组件,这不难理解。
1.4 路径的表示
组件依赖图构建成功后,如何表示从顶层组件到目标组件这一段路径呢?
单从顶层组件来说,肯定是从节点出发,然后通过一个 key 定位到顶层组件,如果要抽象这个行为,显然是传入一个参数经过运算后得到结果,可以使用函数式接口 Function
来表示,入参是节点对象,返回值是顶层组件。
在 Jackson 中,可以使用 JsonNode
来表示 JSON 中的任意节点,包括布尔、数值、文本、对象、数组等等类型的节点。
对于顶层组件,可以认为节点是它的父级组件,那么 Function
的入参就是节点,可以用 JsonNode
表示,出参是顶级组件,因此也可以设置为 JsonNode
,但更好的方式是选择 List<Optional<JsonNode>>
作为出参,因为:
- 一个节点也可以有多个相同的顶层组件
- 这些顶层组件并不是一定存在的
综上所述,使用 Function<JsonNode, List<Optional<JsonNode>>>
来表示从节点定位到顶层组件的行为,而对于从组件定位到其子组件,也可以继续沿用。
这个 Function
的出入参可以认为都是 JsonNode
,如果可以将这些 Function
进行合并,那么不就可以表示从顶层组件到目标组件的路径的吗?
或许 JsonNode
难以理解,换成 Integer
呢?比如 Function<Integer, List<Optional<Integer>>>
:
上述代码就是本文的核心:reduce function。
将 Integer
换成 JsonNode
,将定位的行为 reduce 后,传入表示节点信息的 JsonNode
,得到包含目标组件的 List<Optional<JsonNode>>
,再对 List
进行遍历,不就能得到每个目标组件了吗?
2. 接口定义
2.1 顶级接口
组件标识
对于每个组件来说,它们都有自己的标识,用于区分彼此,定义 ComponentIdentity
接口用于获取组件标识:
1 | public interface ComponentIdentity { |
MyComponent
是组件对象化后的基类。
组件处理器
定位到目标组件后,需要对目标组件进行处理,使用统一的处理方式,定义 SimpleComponentHandler
接口用于实现每种组件的处理方式。为了区分不同组件的处理方式,令 SimpleComponentHandler
继承 ComponentIdentity
:
1 | public interface SimpleComponentHandler extends ComponentIdentity { |
2.2 组件定位器存储器
前文已经讨论过使用 Function
来定义从父级组件(或节点)中获取组件的行为,对于某一组件,从父级组件(或节点)定位到其自身的行为极有可能不止一种,也就是说一种组件可能被不同的组件(或节点)依赖。这种定位的行为需要得到统一的存储,定义存储器实现这样的功能。
对于顶层组件来说,它可以被多种节点包含;对于所有组件来说,它们可以拥有多种子组件。
显然需要两种存储器,分别是 TopLevelComponentLocatorStore
和 ComponentLocatorStore
,前者存储从节点定位到顶层组件的行为,后者用于存储从父级组件定位到子组件的行为。
两种存储器中都定义了添加定位器、获取定位器、获取定位器的 key 集合共三种抽象方法。
2.3 组件定位器
组件定位器
对于每个组件来说,它都有若干个子组件组成,当然这个“若干”可以是 Zero。
如果要对组件进行分类,可以分为列表组件和非列表组件。
定义 ComponentLocator
接口,它提供 判断组件是否为列表组件 和 初始化子组件定位器 的抽象方法。
ComponentLocator
接口继承了以下接口:
ComponentLocatorStore
:用于存储定位子组件的行为;ComponentIdentity
:每种定位器定位的目标组件;SmartInitializingSingleton
:由 Spring 提供的接口,其抽象方法afterSingletonsInstantiated()
将在单例 Bean 初始化完成后执行,可以执行初始化子组件定位器的逻辑。
除此之外,ComponentLocator
接口还提供了默认方法 handleSubComponent()
用于处理其每个子组件。
classDiagram
direction BT
class ComponentIdentity {
<<Interface>>
}
class ComponentLocator {
<<Interface>>
}
class ComponentLocatorStore {
<<Interface>>
}
class SmartInitializingSingleton {
<<Interface>>
}
ComponentLocator --> ComponentIdentity
ComponentLocator --> ComponentLocatorStore
ComponentLocator --> SmartInitializingSingleton
顶层组件定位器
顾名思义,用于定位顶层组件,命名为 TopLevelComponentLocator
,提供 初始化(顶层)组件定位器 的抽象方法。
TopLevelComponentLocator
接口继承了以下接口:
TopLevelComponentLocatorStore
:用于存储定位顶层组件的行为;ComponentLocator
:顶层组件也是组件,它也可能有子组件;SmartInitializingSingleton
:执行初始化(顶层)组件定位器的逻辑。
classDiagram
direction BT
class ComponentIdentity {
<<Interface>>
}
class ComponentLocator {
<<Interface>>
}
class ComponentLocatorStore {
<<Interface>>
}
class SmartInitializingSingleton {
<<Interface>>
}
class TopLevelComponentLocator {
<<Interface>>
}
class TopLevelComponentLocatorStore {
<<Interface>>
}
ComponentLocator --> ComponentIdentity
ComponentLocator --> ComponentLocatorStore
ComponentLocator --> SmartInitializingSingleton
TopLevelComponentLocator --> ComponentLocator
TopLevelComponentLocator --> SmartInitializingSingleton
TopLevelComponentLocator --> TopLevelComponentLocatorStore
2.4 组件处理器
定位到目标组件后,需要根据业务需求对其进行特定的处理,无论是顶层组件还是非顶层组件,都应该有这样的逻辑。
定义 ComponentHandler
接口,该接口继承 SimpleComponentHandler
和 ComponentLocator
接口,用于在定位到任意组件后,具备处理目标组件的能力。该接口只是一个标记接口,不提供任何抽象方法:
1 | public interface ComponentHandler extends SimpleComponentHandler, ComponentLocator { |
classDiagram
direction BT
class ComponentHandler {
<<Interface>>
}
class ComponentIdentity {
<<Interface>>
}
class ComponentLocator {
<<Interface>>
}
class ComponentLocatorStore {
<<Interface>>
}
class SimpleComponentHandler {
<<Interface>>
}
class SmartInitializingSingleton {
<<Interface>>
}
ComponentHandler --> ComponentLocator
ComponentHandler --> SimpleComponentHandler
ComponentLocator --> ComponentIdentity
ComponentLocator --> ComponentLocatorStore
ComponentLocator --> SmartInitializingSingleton
SimpleComponentHandler --> ComponentIdentity
定义 TopLevelComponentHandler
接口,该接口继承 ComponentHandler
和 TopLevelComponentLocator
,用于在定位到顶层组件后能够对其进行处理。该接口同样是一个标记接口,不提供任何抽象方法:
1 | public interface TopLevelComponentHandler extends ComponentHandler, TopLevelComponentLocator { |
classDiagram
direction BT
class ComponentHandler {
<<Interface>>
}
class ComponentIdentity {
<<Interface>>
}
class ComponentLocator {
<<Interface>>
}
class ComponentLocatorStore {
<<Interface>>
}
class SimpleComponentHandler {
<<Interface>>
}
class SmartInitializingSingleton {
<<Interface>>
}
class TopLevelComponentHandler {
<<Interface>>
}
class TopLevelComponentLocator {
<<Interface>>
}
class TopLevelComponentLocatorStore {
<<Interface>>
}
ComponentHandler --> ComponentLocator
ComponentHandler --> SimpleComponentHandler
ComponentLocator --> ComponentIdentity
ComponentLocator --> ComponentLocatorStore
ComponentLocator --> SmartInitializingSingleton
SimpleComponentHandler --> ComponentIdentity
TopLevelComponentHandler --> ComponentHandler
TopLevelComponentHandler --> TopLevelComponentLocator
TopLevelComponentLocator --> ComponentLocator
TopLevelComponentLocator --> SmartInitializingSingleton
TopLevelComponentLocator --> TopLevelComponentLocatorStore
3. 接口实现
3.1 抽象组件处理器
如果对组件进行分类,可以分成 列表组件 与 非列表组件,也可以分成 顶层组件 和 非顶层组件。
将两种分类结果进行排列组合,形成共四种类型,比如非列表非顶层组件,每种类型都有自己的处理方式。
非列表非顶层组件处理器
定义 BaseSimpleComponentHandler
抽象类用于处理非列表非顶层组件。
该类实现 ComponentHandler
接口即可。
类中声明 ComponentLocatorStore
类型的成员变量,将存储定位组件的行为的实现委派给 ComponentLocatorStore
的默认实现类,而不是由其本身完成。
列表非顶层组件处理器
定义 BaseSimpleArrayComponentHandler
抽象类用于处理列表非顶层组件。
该类也实现了 ComponentHandler
接口,除此之外还实现了 Spring 提供的 ApplicationContextAware
接口,用于获取 ApplicationContext
对象,提供获取 Bean 的方式。
对列表组件来说,其组件标识(ComponentIdentity)就是列表中每项组件的组件标识,其子组件就是列表中的每项,这些逻辑可以在此抽象类中完成。
类中同样声明 ComponentLocatorStore
类型的成员变量,将存储定位组件的行为的实现委派给 ComponentLocatorStore
的默认实现类。
除此之外,声明 elementComponent()
抽象方法表明当前列表组件中每项的组件处理器是什么:
1 | protected abstract Class<? extends SimpleComponentHandler> elementComponent(); |
非列表顶层组件处理器
定义 BaseSingleTopLevelComponentHandler
抽象类处理非列表顶层组件。
该类继承 BaseSimpleComponentHandler
,使其具备添加子组件的能力,除此之外实现 TopLevelComponentHandler
接口,拥有处理顶层组件的能力。
类中声明 TopLevelComponentLocatorStore
类型的成员变量,将存储定位顶层组件的行为的实现委派给 TopLevelComponentLocatorStore
的默认实现类。
列表顶层组件处理器
定义 BaseArrayTopLevelComponentHandler
抽象类处理列表顶层组件。
该类继承 BaseSimpleArrayComponentHandler
,使其具备添加子组件的能力,除此之外同样实现 TopLevelComponentHandler
接口,拥有处理顶层组件的能力。
类中也同样声明了 TopLevelComponentLocatorStore
类型的成员变量。
总结
四种处理器按照对组件的分类进行排列组合后得出,针对不同的组件继承不同的抽象类即可。
四种处理器中对存储定位组件的行为的实现都委派给其内部的成员变量,而不是由自身实现。
对列表组件进行特殊处理,认为列表组件是多个同种类型组件的聚合。
3.2 顶层组件处理器分发器
一个顶层组件可能被多种不同类型的节点引用,如果需要处理每种节点中的顶层组件,那么就需要一个分发器,该分发器接收一个节点对象,返回该节点中所有顶层组件的处理器。
定义 TopLevelComponentHandlerDispatcher
接口来完成这样的功能。
classDiagram
direction BT
class ApplicationRunner {
<<Interface>>
}
class DefaultTopLevelComponentHandlerDispatcher
class TopLevelComponentHandlerDispatcher {
<<Interface>>
}
DefaultTopLevelComponentHandlerDispatcher ..> ApplicationRunner
DefaultTopLevelComponentHandlerDispatcher ..> TopLevelComponentHandlerDispatcher
3.3 组件关系收集器
节点由若干个组件组成,组件又可能有子组件,在查找某个组件时,如果该组件是其他组件的子组件,那么应该梳理出从节点到目标组件的定位路径,利用路径实现传入节点对象后返回目标组件列表。
组件被分为列表组件和非列表组件,对列表组件来说,它是某种非列表组件的聚合,它们具有相同的组件标识(ComponentIdentity),因此通过组件标识在若干个节点中查找目标组件时,应该明确查找的是列表组件还是非列表组件。
定义 ComponentRelationshipCollector
类,该类暴露唯一的公共方法:collect()
,用于获取某一组件的依赖关系:
1 | /** |
通过图建立组件的依赖关系后,获取目标组件的依赖关系问题可以转化为求解图中到指定节点的所有路径问题,具体实现参考 pers.mofan.component.util.GraphUtils#getAllPath2TargetNode()
方法。
获取组件的依赖关系后能够得到从父组件到目标组件的一个集合,比如:
B 类型节点中有一个顶层组件 C,该组件有一个子组件 A,子组件 A 恰好也是其中节点中的一个顶层组件。
B 类型节点可以近似作为顶层组件 C 的父组件,从 B 类型节点到组件 A 有一条路径,使用 List
表示这条路径,List
中的每项都是 Function<JsonNode, List<Optional<JsonNode>>>
,将这个 List
进行 reduce 得到单个 Function<JsonNode, List<Optional<JsonNode>>>
后,就可以实现传入一个节点对象最终返回该节点中所有目标组件的一个集合(实现参考【1.4 路径的表示】或者 pers.mofan.component.lookup.ComponentRelationshipCollector#reduceFunctions()
方法)。
3.4 组件查找器
定义 ComponentLookup
接口,用于查找节点下所有目标组件。
该接口继承 ComponentIdentity
接口,获取查找的目标组件标识。
ComponentLookup
接口有个很重要的抽象实现 BaseComponentLookup
,在该类中实现通过目标组件标识在节点中获取目标组件的逻辑。
3.5 组件查找器分发器
组件查找器中收集了从不同类型节点到目标组件的路径信息,还需要一个分发器将给定的组件标识分发到不同的组件查找器,组件查找器分发器由 ComponentLookupDispatcher
实现。
4. 运行与测试
目标数据存放在 test.json
文件中,其中包含一个 JSON 数组,内部数据与【1.1 组件的划分】中给出的 JSON 对象一样。
测试类名为 ReduceFunctionsTest
,内部包含两个测试方法,用于测试:
- 处理节点中所有组件
- 查找目标组件
5. 总结
本文从构思到完成历经三周,终究还是没能战胜懒惰。😔
本文的重点有三个:
- 接口的抽象
- 求图中到目标节点的所有路径
- 如何 reduce
Function
单个分开都比较简单,最关键的是要想到:
- 将从父组件定位到子组件的行为抽象成一个
Function
也只有将这样的行为抽象成 Function
后,后续才能求出组件依赖关系,根据依赖关系对 Function
进行 reduce,最终实现通过传入目标节点和目标组件标识得到节点中所有目标组件。
小弟大言不惭地认为本文可以作为 Java8 函数式接口的使用范本,除此之外在实现中还使用了多个 Spring 拓展接口,因此还能进一步上升到 Java8 函数式接口与 Spring 拓展点的使用范本?🤣
在实现中也存在一些问题,最大的问题就是对接口抽象不够简洁、依赖关系比较混乱,很多底层实现重复对某一接口进行了实现,我认为这是本人水平不够的象征,还需继续努力,希望未来的某天能够写出堪比 Spring 的接口设计。💪
除此之外在功能完成上也有缺陷,不支持自依赖的组件,即一个组件的子组件是它自身,但我的实际需求中没有这样的场景, 就先搁置着吧。😶
本文是我第一篇在实际开发中提炼出的博文,水平不高,仅仅作为总结,在书写过程中不断反省,希望以后的编码能够在此基础上得到提升,最终达到“诗一样的”代码!👊