意图 Intent

抽象文档设计模式是一种结构设计模式,旨在通过为各种文档类型定义公共接口,提供一种一致的方式来处理分层和树状数据结构。它将核心文档结构与特定的数据格式分离,实现动态更新和简化维护。

The Abstract Document design pattern is a structural design pattern that aims to provide a consistent way to handle hierarchical and tree-like data structures by defining a common interface for various document types. It separates the core document structure from specific data formats, enabling dynamic updates and simplified maintenance.

解释 Explanation

抽象文档模式允许处理附加的非静态属性。此模式使用特性的概念来实现类型安全,并将不同类的属性分离到一组接口中。

The Abstract Document pattern enables handling additional, non-static properties. This pattern uses concept of traits to enable type safety and separate properties of different classes into set of interfaces.

真实世界的例子 Real world example

想象一下,在一个图书馆系统中,书籍可以有不同的格式和属性:实体书、电子书和有声读物。每种格式都有独特的属性,如实体书的页数、电子书的文件大小和有声读物的持续时间。摘要文档设计模式允许图书馆系统灵活地管理这些不同的格式。通过使用这种模式,系统可以动态存储和检索属性,而不需要为每种书籍类型提供刚性结构,从而在未来更容易添加新的格式或属性,而不会对代码库进行重大更改。

Imagine a library system where books can have different formats and attributes: physical books, eBooks, and audiobooks. Each format has unique properties, such as page count for physical books, file size for eBooks, and duration for audiobooks. The Abstract Document design pattern allows the library system to manage these diverse formats flexibly. By using this pattern, the system can store and retrieve properties dynamically, without needing a rigid structure for each book type, making it easier to add new formats or attributes in the future without significant changes to the codebase.

用简单的话说 In plain words

抽象文档模式允许在对象不知情的情况下将属性附加到对象。

Abstract Document pattern allows attaching properties to objects without them knowing about it.

维基百科称 Wikipedia says

一种面向对象的结构设计模式,用于在松散类型的键值存储中组织对象,并使用类型化视图公开数据。
该模式的目的是在强类型语言中的组件之间实现高度的灵活性,在这种语言中,可以动态地将新属性添加到对象树中,而不会失去类型安全的支持。
该模式利用特征将类的不同属性分离到不同的接口中。

An object-oriented structural design pattern for organizing objects in loosely typed key-value stores and exposing the data using typed views. The purpose of the pattern is to achieve a high degree of flexibility between components in a strongly typed language where new properties can be added to the object-tree on the fly, without losing the support of type-safety. The pattern makes use of traits to separate different properties of a class into different interfaces.

程序性示例 Programmatic Example

考虑一辆由多个部件组成的汽车。
然而,我们不知道具体的汽车是否真的拥有所有部件,或者只是其中的一部分。
我们的汽车动力十足,非常灵活。

Consider a car that consists of multiple parts. However, we don't know if the specific car really has all the parts, or just some of them. Our cars are dynamic and extremely flexible.

我们先定义基类DocumentAbstractDocument
它们基本上使对象包含一个属性贴图和任意数量的子对象。

Let's first define the base classes Document and AbstractDocument. They basically make the object hold a property map and any amount of child objects.

public interface Document {

    Void put(String key, Object value);

    Object get(String key);

    <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor);
}

public abstract class AbstractDocument implements Document {

    private final Map<String, Object> properties;

    protected AbstractDocument(Map<String, Object> properties) {
        Objects.requireNonNull(properties, "properties map is required");
        this.properties = properties;
    }

    @Override
    public Void put(String key, Object value) {
        properties.put(key, value);
        return null;
    }

    @Override
    public Object get(String key) {
        return properties.get(key);
    }

    @Override
    public <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) {
        return Stream.ofNullable(get(key))
                .filter(Objects::nonNull)
                .map(el -> (List<Map<String, Object>>) el)
                .findAny()
                .stream()
                .flatMap(Collection::stream)
                .map(constructor);
    }
    
    // Other properties and methods...
}

接下来,我们定义一个枚举Property和一组用于类型、价格、型号和部件的接口。
这允许我们创建Car类的静态接口。

Next we define an enum Property and a set of interfaces for type, price, model and parts. This allows us to create static looking interface to our Car class.

public enum Property {

    PARTS, TYPE, PRICE, MODEL
}

public interface HasType extends Document {

    default Optional<String> getType() {
        return Optional.ofNullable((String) get(Property.TYPE.toString()));
    }
}

public interface HasPrice extends Document {

    default Optional<Number> getPrice() {
        return Optional.ofNullable((Number) get(Property.PRICE.toString()));
    }
}

public interface HasModel extends Document {

    default Optional<String> getModel() {
        return Optional.ofNullable((String) get(Property.MODEL.toString()));
    }
}

public interface HasParts extends Document {

    default Stream<Part> getParts() {
        return children(Property.PARTS.toString(), Part::new);
    }
}

现在我们准备好介绍这辆车了。

Now we are ready to introduce the Car.

public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts {

    public Car(Map<String, Object> properties) {
        super(properties);
    }
}

最后,在一个完整的例子中,我们将介绍如何构造和使用car

And finally here's how we construct and use the Car in a full example.

  public static void main(String[] args) {
    LOGGER.info("Constructing parts and car");

    var wheelProperties = Map.of(
            Property.TYPE.toString(), "wheel",
            Property.MODEL.toString(), "15C",
            Property.PRICE.toString(), 100L);

    var doorProperties = Map.of(
            Property.TYPE.toString(), "door",
            Property.MODEL.toString(), "Lambo",
            Property.PRICE.toString(), 300L);

    var carProperties = Map.of(
            Property.MODEL.toString(), "300SL",
            Property.PRICE.toString(), 10000L,
            Property.PARTS.toString(), List.of(wheelProperties, doorProperties));

    var car = new Car(carProperties);

    LOGGER.info("Here is our car:");
    LOGGER.info("-> model: {}", car.getModel().orElseThrow());
    LOGGER.info("-> price: {}", car.getPrice().orElseThrow());
    LOGGER.info("-> parts: ");
    car.getParts().forEach(p -> LOGGER.info("\t{}/{}/{}",
            p.getType().orElse(null),
            p.getModel().orElse(null),
            p.getPrice().orElse(null))
    );
}

The program output:

07:21:57.391 [main] INFO com.iluwatar.abstractdocument.App -- Constructing parts and car
07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- Here is our car:
07:21:57.393 [main] INFO com.iluwatar.abstractdocument.App -- -> model: 300SL
07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> price: 10000
07:21:57.394 [main] INFO com.iluwatar.abstractdocument.App -- -> parts: 
07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App --     wheel/15C/100
07:21:57.395 [main] INFO com.iluwatar.abstractdocument.App --     door/Lambo/300

类图 Class diagram

Abstract Document

适用性 Applicability

此模式在具有不同类型的文档的场景中特别有用,这些文档共享一些共同的属性或行为,但也具有特定于其个别类型的独特属性或行为。下面是一些可以应用抽象文档设计模式的场景:

  • 内容管理系统(CMS):在CMS中,您可能有各种类型的内容,如文章、图像、视频等。每种类型的内容可以具有创建日期、作者和标签等共享属性,也可以具有特定属性,如图像的图像尺寸或视频的视频时长。
  • 文件系统:如果您设计的文件系统需要管理不同类型的文件,如文档、图像、音频文件和目录,抽象文档模式可以帮助提供一致的方式来访问文件大小、创建日期等属性,同时允许特定的属性,如图像分辨率或音频持续时间。
  • 电子商务系统:电子商务平台可能有不同的产品类型,如实体产品、数字下载和订阅。每种类型可以共享共同的属性,如名称、价格和描述,而具有独特的属性,如实体产品的运输重量或数字产品的下载链接。
  • 医疗记录系统:在医疗保健中,患者记录可能包括各种类型的数据,如人口统计数据、病史、测试结果和处方。
    抽象文档模式可以帮助管理共享属性,如患者ID和出生日期,同时适应特定属性,如测试结果或处方药物。
  • 配置管理:在处理软件应用程序的配置设置时,可以有不同类型的配置元素,每个配置元素都有自己的一组属性。
    抽象文档模式可用于管理这些配置元素,同时确保以一致的方式访问和操作它们的属性。
  • 教育平台:教育系统可能有各种类型的学习材料,如基于文本的内容、视频、测验和作业。
    标题、作者和出版日期等公共属性可以共享,而视频时长或作业截止日期等独特属性可以特定于每种类型。
  • 项目管理工具:在项目管理应用程序中,您可能有不同类型的任务,如待办事项、里程碑和问题。
    抽象文档模式可用于处理一般属性,如任务名称和受理人,同时允许特定属性,如里程碑日期或问题优先级。
  • 文件具有多样化和不断演变的属性结构。
  • 动态添加新属性是一种常见要求。
  • 将数据访问与特定格式脱钩至关重要。
  • 可维护性和灵活性对代码库至关重要。

This pattern is particularly useful in scenarios where you have different types of documents that share some common attributes or behaviors, but also have unique attributes or behaviors specific to their individual types. Here are some scenarios where the Abstract Document design pattern can be applicable:

  • Content Management Systems (CMS): In a CMS, you might have various types of content such as articles, images, videos, etc. Each type of content could have shared attributes like creation date, author, and tags, while also having specific attributes like image dimensions for images or video duration for videos.
  • File Systems: If you're designing a file system where different types of files need to be managed, such as documents, images, audio files, and directories, the Abstract Document pattern can help provide a consistent way to access attributes like file size, creation date, etc., while allowing for specific attributes like image resolution or audio duration.
  • E-commerce Systems: An e-commerce platform might have different product types such as physical products, digital downloads, and subscriptions. Each type could share common attributes like name, price, and description, while having unique attributes like shipping weight for physical products or download link for digital products.
  • Medical Records Systems: In healthcare, patient records might include various types of data such as demographics, medical history, test results, and prescriptions. The Abstract Document pattern can help manage shared attributes like patient ID and date of birth, while accommodating specialized attributes like test results or prescribed medications.
  • Configuration Management: When dealing with configuration settings for software applications, there can be different types of configuration elements, each with its own set of attributes. The Abstract Document pattern can be used to manage these configuration elements while ensuring a consistent way to access and manipulate their attributes.
  • Educational Platforms: Educational systems might have various types of learning materials such as text-based content, videos, quizzes, and assignments. Common attributes like title, author, and publication date can be shared, while unique attributes like video duration or assignment due dates can be specific to each type.
  • Project Management Tools: In project management applications, you could have different types of tasks like to-do items, milestones, and issues. The Abstract Document pattern could be used to handle general attributes like task name and assignee, while allowing for specific attributes like milestone date or issue priority.
  • Documents have diverse and evolving attribute structures.
  • Dynamically adding new properties is a common requirement.
  • Decoupling data access from specific formats is crucial.
  • Maintainability and flexibility are critical for the codebase.

抽象文档设计模式背后的关键思想是提供一种灵活且可扩展的方式来管理具有共享和不同属性的不同类型的文档或实体。
通过定义公共接口并跨各种文档类型实现它,您可以实现一种更有组织、更一致的方法来处理复杂的数据结构。

The key idea behind the Abstract Document design pattern is to provide a flexible and extensible way to manage different types of documents or entities with shared and distinct attributes. By defining a common interface and implementing it across various document types, you can achieve a more organized and consistent approach to handling complex data structures.

后遗症 Consequences

优点:

  • 灵活性:适应不同的文档结构和属性。
  • 可扩展性:在不破坏现有代码的情况下动态添加新属性。
  • 可维护性:由于关注点的分离,促进了干净和可适应的代码。
  • 可重用性:类型化视图允许代码重用以访问特定的属性类型。

Benefits:

  • Flexibility: Accommodates varied document structures and properties.
  • Extensibility: Dynamically add new attributes without breaking existing code.
  • Maintainability: Promotes clean and adaptable code due to separation of concerns.
  • Reusability: Typed views enable code reuse for accessing specific attribute types.

权衡:

  • 复杂性:需要定义接口和视图,增加实现开销。
  • 性能:与直接数据访问相比,可能会带来轻微的性能开销。

Trade-offs:

  • Complexity: Requires defining interfaces and views, adding implementation overhead.
  • Performance: Might introduce slight performance overhead compared to direct data access.

Credits

最后修改:2024 年 05 月 28 日
如果觉得我的文章对你有用,请随意赞赏