抽象文档模式

抽象文档模式

在软件开发中,我们经常需要处理半结构化数据(如JSON、XML、文档数据库中的文档)。这类数据的特点是结构灵活,可能存在嵌套关系,且字段可能动态变化。传统的面向对象设计可能需要为每种数据结构定义大量类,导致代码冗余和维护困难。这时候,抽象文档模式(Abstract Document Pattern) 就能派上用场。

本文将通过一个完整的Java案例,详细讲解抽象文档模式的实现原理、设计思路和实际应用,帮助你掌握这一处理半结构化数据的利器。


一、抽象文档模式核心思想

抽象文档模式的核心是:将半结构化数据抽象为统一的文档对象,通过标准化的接口访问属性和子文档,同时保持对数据结构变化的灵活性。它通过以下两个关键设计实现这一目标:

  1. 统一的文档接口(Document):定义属性访问、子文档管理等核心操作,屏蔽底层数据格式(如Map、JSON对象)的差异。
  2. 不可变的基础实现(AbstractDocument):通过封装不可变的属性存储(如UnmodifiableMap),确保线程安全和数据一致性。

二、代码结构全景图

在开始详细分析前,先看一下代码的整体结构:

抽象文档模式/
├── Document.java          # 文档核心接口
├── AbstractDocument.java  # 抽象文档基础实现(不可变)
├── Property.java          # 属性键枚举(统一管理键名)
└── domain/                # 领域模型
    ├── Car.java           # 汽车文档类
    ├── Part.java          # 零件文档类
    ├── HasType.java       # 类型属性访问接口
    ├── HasModel.java      # 型号属性访问接口
    ├── HasPrice.java      # 价格属性访问接口
    └── HasParts.java      # 子零件访问接口

三、核心组件详解

1. Document接口:定义文档行为契约

Document接口是所有文档对象的“契约”,定义了三类核心操作:

(1)属性存储操作
    /**
     * 设置属性值(注意:部分实现类可能不支持修改)
     *
     * @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,需类型转换)。
(2)类型安全的属性获取(通用方法)
    /**
     * 安全获取指定类型的属性值(通用方法)
     *
     * @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()

(3)子文档管理
  /**
     * 获取子文档流(优化版)
     *
     * @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转换为具体的文档对象(如PartCar)。这一设计完美解决了半结构化数据中嵌套结构的访问问题。


2. AbstractDocument:不可变文档的基础实现

AbstractDocumentDocument接口的抽象实现类,核心特点是不可变性(构造后属性不可修改),适合多线程环境。

(1)不可变属性存储
    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());
    }

  • 构造时强制复制输入的Map,避免外部代码修改内部状态。
  • properties字段被声明为final,且通过Collections.unmodifiableMap包装,确保线程安全。
(2)接口方法的实现
   /**
     * 设置属性值(不可变实现中抛出异常)
     *
     * @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。
(3)只读访问
    /**
     * 获取属性存储Map(只读视图)
     *
     * @return 不可修改的属性Map
     */
    protected Map<String, Object> getProperties() {
        return properties;
    }

提供受保护的getProperties方法,允许子类访问内部属性(但无法修改)。


3. Property枚举:统一管理属性键名

Property枚举定义了所有可能的文档属性键(如partstypeprice),并通过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"),只需修改枚举的构造参数即可,无需修改所有使用该键的代码。


4. 领域模型:通过接口扩展功能

在抽象文档模式中,具体的文档类型(如CarPart)通过继承AbstractDocument并实现功能接口(如HasTypeHasModel)来扩展能力。这些功能接口通过默认方法封装了特定属性的获取逻辑。

(1)功能接口示例:HasType
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能力。

(2)领域类实现:Part和Car

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

5. HasParts接口:处理嵌套子文档

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方法的执行流程如下:

  1. 提取原始值:从当前文档中获取Property.PARTS.getKey()对应的值(即零件列表)。
  2. 类型检查:如果值不存在,返回空流;如果不是List类型,记录警告并返回空流。
  3. 元素转换:遍历List中的每个元素(预期是Map),将其转换为Part对象:
    • 如果元素不是Map类型,记录警告并跳过。
    • 调用Part的构造函数(传入Map)创建实例。
    • 捕获转换过程中的异常(如属性缺失),记录错误日志。
  4. 过滤空值:最终返回仅包含有效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

关键步骤解析
  1. 零件构造:通过Map.of创建零件的属性Map,传入Part构造函数。Part继承自AbstractDocument,内部存储不可变的属性。
  2. 汽车构造:汽车的parts属性是一个List,每个Map对应一个零件的属性。Car类通过HasParts接口的getParts方法,将这些Map转换为Part对象流。
  3. 属性访问:通过功能接口的方法(如getModel()getPrice())安全获取属性值,无需手动类型转换。

五、抽象文档模式的优缺点

优点

  1. 灵活处理半结构化数据:无需为每种数据结构定义大量类,通过统一的Document接口和扩展功能接口即可处理嵌套、动态的文档结构。
  2. 线程安全AbstractDocument的不可变设计确保了多线程环境下的安全性,避免了并发修改问题。
  3. 可维护性高:属性键通过Property枚举统一管理,修改键名只需调整枚举,无需修改业务代码;功能接口通过默认方法封装通用逻辑,减少重复代码。
  4. 扩展性强:新增属性或功能只需添加新的功能接口(如HasColor),现有文档类可通过实现新接口扩展能力,符合开闭原则。

缺点

  1. 类型转换成本:虽然getProperty提供了类型安全的检查,但频繁的Optional操作可能增加代码复杂度(需处理空值)。
  2. 性能损耗:嵌套文档的转换(如children方法中的流处理)可能引入一定的性能开销,对于超大规模数据的处理需谨慎优化。
  3. 学习成本:需要理解抽象文档模式的设计思想,以及Document接口、功能接口、抽象实现类之间的关系,对新手不够友好。

六、适用场景

抽象文档模式适用于以下场景:

  • 半结构化数据处理:如JSON/XML配置文件解析、文档数据库(MongoDB)的文档操作。
  • 动态数据模型:数据结构可能动态变化(如新增字段),需要灵活扩展的场景。
  • 嵌套结构访问:需要频繁访问嵌套子文档(如订单-商品-规格的层级关系)。
  • 多团队协作:不同团队负责不同的文档类型(如汽车团队、零件团队),通过统一接口降低耦合。

七、总结

抽象文档模式通过统一的文档接口、不可变的基础实现和功能接口的扩展,为半结构化数据的管理提供了一种优雅的解决方案。它平衡了灵活性与规范性,使得代码更易于维护和扩展。在实际项目中,可以根据具体需求调整接口设计和抽象层次,充分发挥这一模式的优势。

你可能感兴趣的:(设计模式,开发语言,java)