Browser docs

Abstract Document

Intent

Use dynamic properties and achieve flexibility of untyped languages while keeping type-safety.

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

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.

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.[1] The term “document” is inspired from document-oriented databases.

Programmatic Example

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.

 1public interface Document {
 2
 3  Void put(String key, Object value);
 4
 5  Object get(String key);
 6
 7  <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor);
 8}
 9
10public abstract class AbstractDocument implements Document {
11
12  private final Map<String, Object> properties;
13
14  protected AbstractDocument(Map<String, Object> properties) {
15    Objects.requireNonNull(properties, "properties map is required");
16    this.properties = properties;
17  }
18
19  @Override
20  public Void put(String key, Object value) {
21    properties.put(key, value);
22    return null;
23  }
24
25  @Override
26  public Object get(String key) {
27    return properties.get(key);
28  }
29
30  @Override
31  public <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) {
32    return Stream.ofNullable(get(key))
33        .filter(Objects::nonNull)
34        .map(el -> (List<Map<String, Object>>) el)
35        .findAny()
36        .stream()
37        .flatMap(Collection::stream)
38        .map(constructor);
39  }
40  ...
41}

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.

 1public enum Property {
 2
 3  PARTS, TYPE, PRICE, MODEL
 4}
 5
 6public interface HasType extends Document {
 7
 8  default Optional<String> getType() {
 9    return Optional.ofNullable((String) get(Property.TYPE.toString()));
10  }
11}
12
13public interface HasPrice extends Document {
14
15  default Optional<Number> getPrice() {
16    return Optional.ofNullable((Number) get(Property.PRICE.toString()));
17  }
18}
19public interface HasModel extends Document {
20
21  default Optional<String> getModel() {
22    return Optional.ofNullable((String) get(Property.MODEL.toString()));
23  }
24}
25
26public interface HasParts extends Document {
27
28  default Stream<Part> getParts() {
29    return children(Property.PARTS.toString(), Part::new);
30  }
31}

Now we are ready to introduce the Car.

1public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts {
2
3  public Car(Map<String, Object> properties) {
4    super(properties);
5  }
6}

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

 1    LOGGER.info("Constructing parts and car");
 2
 3    var wheelProperties = Map.of(
 4        Property.TYPE.toString(), "wheel",
 5        Property.MODEL.toString(), "15C",
 6        Property.PRICE.toString(), 100L);
 7
 8    var doorProperties = Map.of(
 9        Property.TYPE.toString(), "door",
10        Property.MODEL.toString(), "Lambo",
11        Property.PRICE.toString(), 300L);
12
13    var carProperties = Map.of(
14        Property.MODEL.toString(), "300SL",
15        Property.PRICE.toString(), 10000L,
16        Property.PARTS.toString(), List.of(wheelProperties, doorProperties));
17
18    var car = new Car(carProperties);
19
20    LOGGER.info("Here is our car:");
21    LOGGER.info("-> model: {}", car.getModel().orElseThrow());
22    LOGGER.info("-> price: {}", car.getPrice().orElseThrow());
23    LOGGER.info("-> parts: ");
24    car.getParts().forEach(p -> LOGGER.info("\t{}/{}/{}",
25        p.getType().orElse(null),
26        p.getModel().orElse(null),
27        p.getPrice().orElse(null))
28    );
29
30    // Constructing parts and car
31    // Here is our car:
32    // model: 300SL
33    // price: 10000
34    // parts: 
35    // wheel/15C/100
36    // door/Lambo/300

Class diagram

alt text

Applicability

Use the Abstract Document Pattern when

  • There is a need to add new properties on the fly
  • You want a flexible way to organize domain in tree like structure
  • You want more loosely coupled system

Credits