Skip to content

Commit 8364b28

Browse files
committed
iluwatar#590 explanation for Abstract Document
1 parent 417f21e commit 8364b28

File tree

4 files changed

+168
-10
lines changed

4 files changed

+168
-10
lines changed

abstract-document/README.md

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,182 @@ tags:
99
---
1010

1111
## Intent
12-
Achieve flexibility of untyped languages and keep the type-safety
12+
13+
Use dynamic properties and achieve flexibility of untyped languages while keeping type-safety.
14+
15+
## Explanation
16+
17+
The Abstract Document pattern enables handling additional, non-static properties. This pattern
18+
uses concept of traits to enable type safety and separate properties of different classes into
19+
set of interfaces.
20+
21+
Real world example
22+
23+
> 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.
24+
25+
In plain words
26+
27+
> Abstract Document pattern allows attaching properties to objects without them knowing about it.
28+
29+
Wikipedia says
30+
31+
> An object-oriented structural design pattern for organizing objects in loosely typed key-value stores and exposing
32+
the data using typed views. The purpose of the pattern is to achieve a high degree of flexibility between components
33+
in a strongly typed language where new properties can be added to the object-tree on the fly, without losing the
34+
support of type-safety. The pattern makes use of traits to separate different properties of a class into different
35+
interfaces.
36+
37+
**Programmatic Example**
38+
39+
Let's first define the base classes `Document` and `AbstractDocument`. They basically make the object hold a property
40+
map and any amount of child objects.
41+
42+
```java
43+
public interface Document {
44+
45+
Void put(String key, Object value);
46+
47+
Object get(String key);
48+
49+
<T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor);
50+
}
51+
52+
public abstract class AbstractDocument implements Document {
53+
54+
private final Map<String, Object> properties;
55+
56+
protected AbstractDocument(Map<String, Object> properties) {
57+
Objects.requireNonNull(properties, "properties map is required");
58+
this.properties = properties;
59+
}
60+
61+
@Override
62+
public Void put(String key, Object value) {
63+
properties.put(key, value);
64+
return null;
65+
}
66+
67+
@Override
68+
public Object get(String key) {
69+
return properties.get(key);
70+
}
71+
72+
@Override
73+
public <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) {
74+
return Stream.ofNullable(get(key))
75+
.filter(Objects::nonNull)
76+
.map(el -> (List<Map<String, Object>>) el)
77+
.findAny()
78+
.stream()
79+
.flatMap(Collection::stream)
80+
.map(constructor);
81+
}
82+
...
83+
}
84+
```
85+
Next we define an enum `Property` and a set of interfaces for type, price, model and parts. This allows us to create
86+
static looking interface to our `Car` class.
87+
88+
```java
89+
public enum Property {
90+
91+
PARTS, TYPE, PRICE, MODEL
92+
}
93+
94+
public interface HasType extends Document {
95+
96+
default Optional<String> getType() {
97+
return Optional.ofNullable((String) get(Property.TYPE.toString()));
98+
}
99+
}
100+
101+
public interface HasPrice extends Document {
102+
103+
default Optional<Number> getPrice() {
104+
return Optional.ofNullable((Number) get(Property.PRICE.toString()));
105+
}
106+
}
107+
public interface HasModel extends Document {
108+
109+
default Optional<String> getModel() {
110+
return Optional.ofNullable((String) get(Property.MODEL.toString()));
111+
}
112+
}
113+
114+
public interface HasParts extends Document {
115+
116+
default Stream<Part> getParts() {
117+
return children(Property.PARTS.toString(), Part::new);
118+
}
119+
}
120+
```
121+
122+
Now we are ready to introduce the `Car`.
123+
124+
```java
125+
public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts {
126+
127+
public Car(Map<String, Object> properties) {
128+
super(properties);
129+
}
130+
}
131+
```
132+
133+
And finally here's how we construct and use the `Car` in a full example.
134+
135+
```java
136+
LOGGER.info("Constructing parts and car");
137+
138+
var wheelProperties = Map.of(
139+
Property.TYPE.toString(), "wheel",
140+
Property.MODEL.toString(), "15C",
141+
Property.PRICE.toString(), 100L);
142+
143+
var doorProperties = Map.of(
144+
Property.TYPE.toString(), "door",
145+
Property.MODEL.toString(), "Lambo",
146+
Property.PRICE.toString(), 300L);
147+
148+
var carProperties = Map.of(
149+
Property.MODEL.toString(), "300SL",
150+
Property.PRICE.toString(), 10000L,
151+
Property.PARTS.toString(), List.of(wheelProperties, doorProperties));
152+
153+
var car = new Car(carProperties);
154+
155+
LOGGER.info("Here is our car:");
156+
LOGGER.info("-> model: {}", car.getModel().orElseThrow());
157+
LOGGER.info("-> price: {}", car.getPrice().orElseThrow());
158+
LOGGER.info("-> parts: ");
159+
car.getParts().forEach(p -> LOGGER.info("\t{}/{}/{}",
160+
p.getType().orElse(null),
161+
p.getModel().orElse(null),
162+
p.getPrice().orElse(null))
163+
);
164+
165+
// Constructing parts and car
166+
// Here is our car:
167+
// model: 300SL
168+
// price: 10000
169+
// parts:
170+
// wheel/15C/100
171+
// door/Lambo/300
172+
```
13173
14174
## Class diagram
15-
![alt text](./etc/abstract-document.png "Abstract Document Traits and Domain")
16175
176+
![alt text](./etc/abstract-document.png "Abstract Document Traits and Domain")
17177
18178
## Applicability
19-
Use the Abstract Document Pattern when
20179
21-
* there is a need to add new properties on the fly
22-
* you want a flexible way to organize domain in tree like structure
23-
* you want more loosely coupled system
180+
Use the Abstract Document Pattern when
24181
182+
* There is a need to add new properties on the fly
183+
* You want a flexible way to organize domain in tree like structure
184+
* You want more loosely coupled system
25185
26186
## Credits
27-
187+
``
28188
* [Wikipedia: Abstract Document Pattern](https://en.wikipedia.org/wiki/Abstract_Document_Pattern)
29189
* [Martin Fowler: Dealing with properties](http://martinfowler.com/apsupp/properties.pdf)
190+
* [Pattern-Oriented Software Architecture Volume 4: A Pattern Language for Distributed Computing (v. 4)](https://www.amazon.com/gp/product/0470059028/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0470059028&linkId=e3aacaea7017258acf184f9f3283b492)

abstract-document/src/main/java/com/iluwatar/abstractdocument/domain/HasParts.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
*/
3333
public interface HasParts extends Document {
3434

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

abstract-document/src/main/java/com/iluwatar/abstractdocument/domain/HasPrice.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
*/
3333
public interface HasPrice extends Document {
3434

35-
3635
default Optional<Number> getPrice() {
3736
return Optional.ofNullable((Number) get(Property.PRICE.toString()));
3837
}

abstract-document/src/main/java/com/iluwatar/abstractdocument/domain/HasType.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
*/
3333
public interface HasType extends Document {
3434

35-
3635
default Optional<String> getType() {
3736
return Optional.ofNullable((String) get(Property.TYPE.toString()));
3837
}

0 commit comments

Comments
 (0)