在软件开发中,我们经常需要处理半结构化数据(如JSON、XML、文档数据库中的文档)。这类数据的特点是结构灵活,可能存在嵌套关系,且字段可能动态变化。传统的面向对象设计可能需要为每种数据结构定义大量类,导致代码冗余和维护困难。这时候,抽象文档模式(Abstract Document Pattern) 就能派上用场。
本文将通过一个完整的Java案例,详细讲解抽象文档模式的实现原理、设计思路和实际应用,帮助你掌握这一处理半结构化数据的利器。
抽象文档模式的核心是:将半结构化数据抽象为统一的文档对象,通过标准化的接口访问属性和子文档,同时保持对数据结构变化的灵活性。它通过以下两个关键设计实现这一目标:
在开始详细分析前,先看一下代码的整体结构:
抽象文档模式/
├── Document.java # 文档核心接口
├── AbstractDocument.java # 抽象文档基础实现(不可变)
├── Property.java # 属性键枚举(统一管理键名)
└── domain/ # 领域模型
├── Car.java # 汽车文档类
├── Part.java # 零件文档类
├── HasType.java # 类型属性访问接口
├── HasModel.java # 型号属性访问接口
├── HasPrice.java # 价格属性访问接口
└── HasParts.java # 子零件访问接口
Document
接口是所有文档对象的“契约”,定义了三类核心操作:
/**
* 设置属性值(注意:部分实现类可能不支持修改)
*
* @param key 属性键(对应Property枚举的getKey()结果)
* @param value 属性值
* @return null(保留与Map.put一致的方法签名)
*/
Void put(String key, Object value);
/**
* 获取属性值
*
* @param key 属性键
* @return 属性值(可能为null)
*/
Object get(String key);
put
方法用于动态添加/修改属性(但抽象实现中可能禁用)。get
方法用于获取属性值(返回Object,需类型转换)。 /**
* 安全获取指定类型的属性值(通用方法)
*
* @param key 属性键
* @param clazz 目标类型Class对象
* @param 目标类型泛型
* @return 包含目标类型值的Optional(无值或类型不匹配时返回empty)
*/
default <T> Optional<T> getProperty(String key, Class<T> clazz) {
Object value = get(key);
if (value == null) {
Logger.debugF("属性[{}]不存在或值为null", key);
return Optional.empty();
}
if (!clazz.isInstance(value)) {
Logger.warnF("属性[{}]类型不匹配,期望类型:{},实际类型:{}",
key, clazz.getSimpleName(), value.getClass().getSimpleName());
return Optional.empty();
}
return Optional.of(clazz.cast(value));
}
通过默认方法提供类型安全的属性获取,避免手动类型检查和转换的繁琐操作。如果属性不存在或类型不匹配,返回Optional.empty()
。
/**
* 获取子文档流(优化版)
*
* @param key 子文档列表存储键
* @param constructor 子文档构造器(将Map转换为具体文档对象)
* @param 子文档类型泛型
* @return 子文档流(过滤空值并处理类型异常)
*/
default <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) {
Logger.debugF("尝试获取键[{}]对应的子文档列表", key);
Object rawValue = get(key);
if (rawValue == null) {
Logger.debugF("键[{}]无子文档,返回空流", key);
return Stream.empty();
}
if (!(rawValue instanceof List)) {
Logger.warnF("键[{}]的值类型非List,实际类型:{},返回空流",
key, rawValue.getClass().getSimpleName());
return Stream.empty();
}
return ((List<?>) rawValue).stream()
.map(element -> {
if (!(element instanceof Map)) {
Logger.warnF("子文档元素类型非Map,跳过该元素,元素类型:{}",
element != null ? element.getClass().getSimpleName() : "null");
return null;
}
try {
@SuppressWarnings("unchecked")
Map<String, Object> elementMap = (Map<String, Object>) element;
T doc = constructor.apply(elementMap);
Logger.debugF("成功构造子文档:{}", doc);
return doc;
} catch (Exception e) {
Logger.errorF("构造子文档失败,元素:{}", element, e);
return null;
}
})
.filter(Objects::nonNull);
}
children
方法用于处理嵌套的子文档。它从当前文档的key
键对应的值中提取列表,遍历每个元素(预期是Map),并通过constructor
函数将Map转换为具体的文档对象(如Part
或Car
)。这一设计完美解决了半结构化数据中嵌套结构的访问问题。
AbstractDocument
是Document
接口的抽象实现类,核心特点是不可变性(构造后属性不可修改),适合多线程环境。
private final Map<String, Object> properties; // 不可变属性存储
/**
* 构造文档实例(复制输入的属性Map以保证不可变性)
*
* @param properties 初始属性Map(不可为null)
* @throws NullPointerException 若properties为null
*/
protected AbstractDocument(Map<String, Object> properties) {
this.properties = Collections.unmodifiableMap(
new HashMap<>(Objects.requireNonNull(properties, "properties map is required"))
);
log.info("创建抽象文档,初始属性数量:{}", properties.size());
}
properties
字段被声明为final
,且通过Collections.unmodifiableMap
包装,确保线程安全。 /**
* 设置属性值(不可变实现中抛出异常)
*
* @param key 属性键
* @param value 属性值
* @return null
* @throws UnsupportedOperationException 始终抛出此异常
*/
@Override
public Void put(String key, Object value) {
throw new UnsupportedOperationException("不可变文档禁止修改属性");
}
/**
* 获取属性值(委托给内部properties)
*
* @param key 属性键
* @return 属性值(可能为null)
*/
@Override
public Object get(String key) {
return properties.get(key);
}
put
方法直接抛出异常,禁止修改属性(符合不可变设计)。get
方法委托给内部的properties
Map。 /**
* 获取属性存储Map(只读视图)
*
* @return 不可修改的属性Map
*/
protected Map<String, Object> getProperties() {
return properties;
}
提供受保护的getProperties
方法,允许子类访问内部属性(但无法修改)。
Property
枚举定义了所有可能的文档属性键(如parts
、type
、price
),并通过getKey()
方法返回对应的字符串键名。它的核心作用是避免硬编码键名,提高代码可维护性。
public enum Property {
PARTS("parts"), // 零件列表键
TYPE("type"), // 类型键
PRICE("price"), // 价格键
MODEL("model"); // 型号键
private final String key;
Property(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
例如,当需要获取零件的类型时,直接使用Property.TYPE.getKey()
,而不是硬编码字符串"type"
。如果未来需要修改键名(如从"type"
改为"part_type"
),只需修改枚举的构造参数即可,无需修改所有使用该键的代码。
在抽象文档模式中,具体的文档类型(如Car
、Part
)通过继承AbstractDocument
并实现功能接口(如HasType
、HasModel
)来扩展能力。这些功能接口通过默认方法封装了特定属性的获取逻辑。
public interface HasType extends Document {
default Optional<String> getType() {
return getProperty(Property.TYPE.getKey(), String.class);
}
}
HasType
接口声明了getType()
方法,内部调用getProperty
获取Property.TYPE
对应的String类型值。任何实现HasType
的文档类(如Part
)都会自动拥有getType
能力。
Part
(零件)和Car
(汽车)是具体的文档类,它们继承AbstractDocument
并实现多个功能接口:
// Part.java
public class Part extends AbstractDocument implements HasType, HasModel, HasPrice {
public Part(Map<String, Object> properties) {
super(properties);
}
}
// Car.java
public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts {
public Car(Map<String, Object> properties) {
super(properties);
}
// 实现HasParts接口的getParts方法(示例)
@Override
public Stream<Part> getParts() {
return children(Property.PARTS.getKey(), Part::new);
}
}
Part
类:实现了HasType
(类型)、HasModel
(型号)、HasPrice
(价格)接口,可直接调用getType()
、getModel()
等方法获取属性。Car
类:除了上述接口外,还实现了HasParts
接口(获取子零件列表),通过children
方法将parts
键对应的List转换为Stream
。HasParts
接口是处理嵌套结构的关键,它通过children
方法将父文档中的子文档列表转换为具体的文档对象流。
public interface HasParts extends Document {
default Stream<Part> getParts() {
return children(Property.PARTS.getKey(), Part::new);
}
}
children
方法的工作流程假设Car
对象的parts
键对应一个List(如[{"type":"wheel", ...}, {"type":"door", ...}]
),children
方法的执行流程如下:
Property.PARTS.getKey()
对应的值(即零件列表)。Part
对象:
Part
的构造函数(传入Map)创建实例。Part
对象的流。这一设计将复杂的嵌套结构访问逻辑封装在接口中,使用者只需调用car.getParts()
即可获得零件流,无需关心底层转换细节。
通过App
类的main
方法,我们可以直观看到抽象文档模式的使用方式:
public class App {
public static void main(String[] args) {
log.info("===== 开始构造零件和汽车 =====");
// 构造零件属性
Map<String, Object> wheelProps = Map.of(
Property.TYPE.getKey(), "wheel",
Property.MODEL.getKey(), "15C",
Property.PRICE.getKey(), 100L
);
Part wheel = new Part(wheelProps);
log.debug("构造零件:{}", wheel);
Map<String, Object> doorProps = Map.of(
Property.TYPE.getKey(), "door",
Property.MODEL.getKey(), "Lambo",
Property.PRICE.getKey(), 300L
);
Part door = new Part(doorProps);
log.debug("构造零件:{}", door);
// 构造汽车属性(包含零件列表)
Map<String, Object> carProps = Map.of(
Property.MODEL.getKey(), "300SL",
Property.PRICE.getKey(), 10000L,
Property.PARTS.getKey(), List.of(wheelProps, doorProps)
);
Car car = new Car(carProps);
log.info("\n===== 汽车信息 =====");
log.info("型号:{}", car.getModel().orElseThrow());
log.info("价格:{}元", car.getPrice().map(Number::intValue).orElseThrow());
log.info("零件列表:");
car.getParts().forEach(part ->
log.info("\t- 类型:{},型号:{},价格:{}元",
part.getType().orElse("未知"),
part.getModel().orElse("未知"),
part.getPrice().map(Number::intValue).orElse(0))
);
}
}
2025-07-22 02:41:42.271 [main] INFO com.dwl.design_pattern.抽象文档模式.App - ===== 开始构造零件和汽车 =====
Initializing Env static block...
Env static block initialized
[DWL] FAST_THREAD_LOCAL_SIZE: 1024
2025-07-22 02:41:42.458 [main] INFO com.dwl.design_pattern.抽象文档模式.AbstractDocument - 创建抽象文档,初始属性数量:3
2025-07-22 02:41:42.458 [main] INFO com.dwl.design_pattern.抽象文档模式.domain.Part - 创建零件实例,类型:wheel,型号:15C
--------------------------------------------------------------------------------------------------------------------------------------------------
[DEBUG] [2025-07-22 02:41:42:337 CST] [Thread:main] [Time:00:00:00] com.dwl.design_pattern.抽象文档模式.domain.enums.Property.(Property.java:29)
--------------------------------------------------------------------------------------------------------------------------------------------------
初始化Property枚举常量:PARTS -> 键名:parts
2025-07-22 02:41:42.463 [main] DEBUG com.dwl.design_pattern.抽象文档模式.App - 构造零件:Part{type=wheel, model=15C, price=100.0}
2025-07-22 02:41:42.463 [main] INFO com.dwl.design_pattern.抽象文档模式.AbstractDocument - 创建抽象文档,初始属性数量:3
2025-07-22 02:41:42.463 [main] INFO com.dwl.design_pattern.抽象文档模式.domain.Part - 创建零件实例,类型:door,型号:Lambo
2025-07-22 02:41:42.463 [main] DEBUG com.dwl.design_pattern.抽象文档模式.App - 构造零件:Part{type=door, model=Lambo, price=300.0}
--------------------------------------------------------------------------------------------------------------------------------------------------
[DEBUG] [2025-07-22 02:41:42:426 CST] [Thread:main] [Time:00:00:00] com.dwl.design_pattern.抽象文档模式.domain.enums.Property.(Property.java:29)
--------------------------------------------------------------------------------------------------------------------------------------------------
初始化Property枚举常量:TYPE -> 键名:type
2025-07-22 02:41:42.467 [main] INFO com.dwl.design_pattern.抽象文档模式.AbstractDocument - 创建抽象文档,初始属性数量:3
2025-07-22 02:41:42.467 [main] INFO com.dwl.design_pattern.抽象文档模式.domain.Car - 创建汽车实例,型号:300SL
2025-07-22 02:41:42.467 [main] INFO com.dwl.design_pattern.抽象文档模式.App -
===== 汽车信息 =====
2025-07-22 02:41:42.467 [main] INFO com.dwl.design_pattern.抽象文档模式.App - 型号:300SL
--------------------------------------------------------------------------------------------------------------------------------------------------
[DEBUG] [2025-07-22 02:41:42:426 CST] [Thread:main] [Time:00:00:00] com.dwl.design_pattern.抽象文档模式.domain.enums.Property.(Property.java:29)
--------------------------------------------------------------------------------------------------------------------------------------------------
初始化Property枚举常量:PRICE -> 键名:price
2025-07-22 02:41:42.469 [main] INFO com.dwl.design_pattern.抽象文档模式.App - 价格:10000元
2025-07-22 02:41:42.469 [main] INFO com.dwl.design_pattern.抽象文档模式.App - 零件列表:
2025-07-22 02:41:42.474 [main] INFO com.dwl.design_pattern.抽象文档模式.AbstractDocument - 创建抽象文档,初始属性数量:3
2025-07-22 02:41:42.474 [main] INFO com.dwl.design_pattern.抽象文档模式.domain.Part - 创建零件实例,类型:wheel,型号:15C
2025-07-22 02:41:42.475 [main] INFO com.dwl.design_pattern.抽象文档模式.App - - 类型:wheel,型号:15C,价格:100元
2025-07-22 02:41:42.476 [main] INFO com.dwl.design_pattern.抽象文档模式.AbstractDocument - 创建抽象文档,初始属性数量:3
2025-07-22 02:41:42.476 [main] INFO com.dwl.design_pattern.抽象文档模式.domain.Part - 创建零件实例,类型:door,型号:Lambo
2025-07-22 02:41:42.477 [main] INFO com.dwl.design_pattern.抽象文档模式.App - - 类型:door,型号:Lambo,价格:300元
Shutdown hook triggered
--------------------------------------------------------------------------------------------------------------------------------------------------
[DEBUG] [2025-07-22 02:41:42:427 CST] [Thread:main] [Time:00:00:00] com.dwl.design_pattern.抽象文档模式.domain.enums.Property.(Property.java:29)
--------------------------------------------------------------------------------------------------------------------------------------------------
初始化Property枚举常量:MODEL -> 键名:model
[FRAMEWORK] Main logger thread is terminal
Disconnected from the target VM, address: '127.0.0.1:3802', transport: 'socket'
Process finished with exit code 0
Map.of
创建零件的属性Map,传入Part
构造函数。Part
继承自AbstractDocument
,内部存储不可变的属性。parts
属性是一个List,每个Map对应一个零件的属性。Car
类通过HasParts
接口的getParts
方法,将这些Map转换为Part
对象流。getModel()
、getPrice()
)安全获取属性值,无需手动类型转换。Document
接口和扩展功能接口即可处理嵌套、动态的文档结构。AbstractDocument
的不可变设计确保了多线程环境下的安全性,避免了并发修改问题。Property
枚举统一管理,修改键名只需调整枚举,无需修改业务代码;功能接口通过默认方法封装通用逻辑,减少重复代码。HasColor
),现有文档类可通过实现新接口扩展能力,符合开闭原则。getProperty
提供了类型安全的检查,但频繁的Optional
操作可能增加代码复杂度(需处理空值)。children
方法中的流处理)可能引入一定的性能开销,对于超大规模数据的处理需谨慎优化。Document
接口、功能接口、抽象实现类之间的关系,对新手不够友好。抽象文档模式适用于以下场景:
抽象文档模式通过统一的文档接口、不可变的基础实现和功能接口的扩展,为半结构化数据的管理提供了一种优雅的解决方案。它平衡了灵活性与规范性,使得代码更易于维护和扩展。在实际项目中,可以根据具体需求调整接口设计和抽象层次,充分发挥这一模式的优势。