정의
무타입 언어의 유연성을 Java, C와 같은 타입형 언어에서 얻을 수 있도록 한 패턴이다.
구성요소
- Document
- put
- Document가 가진 속성을 추가/변경할 수 있는 메소드다.
- get
- Document가 가진 속성에 접근할 수 있도록 하는 메소드다
- children
- Document의 sub Document에 접근할 수 있도록 하는 메소드다.
- sub document의 타입 안정성을 위해 factory function을 인자로 받는 것이 독특하다.
- BaseDocument
- Treat Interface(e.g Has* interface)
- Implementation class(e.g Car)
- Treat Interface를 선택적으로 구현한 클래스이다.
- 가진 속성에 따라 Treat Interface를 선택적으로 구현한다.
Document.java
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);
}
|
AbstractDocument.java
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) {
Optional<List<Map<String, Object>>> any = Stream.of(get(key)).filter(el -> el != null)
.map(el -> (List<Map<String, Object>>) el).findAny();
return any.isPresent() ? any.get().stream().map(constructor) : Stream.empty();
}
}
|
HasPrice.java
public interface HasPrice extends Document {
String PROPERTY = "price";
default Optional<Number> getPrice() {
return Optional.ofNullable((Number) get(PROPERTY));
}
}
|
HasType.java
public interface HasType extends Document {
String PROPERTY = "type";
default Optional<String> getType() {
return Optional.ofNullable((String) get(PROPERTY));
}
}
|
HasModel.java
public interface HasModel extends Document {
String PROPERTY = "model";
default Optional<String> getModel() {
return Optional.ofNullable((String) get(PROPERTY));
}
}
|
HasParts.java
public interface HasParts extends Document {
String PROPERTY = "parts";
default Stream<Part> getParts() {
return children(PROPERTY, Part::new);
}
}
|
Car.java
public class Car extends AbstractDocument implements HasModel, HasPrice, HasParts {
public Car(Map<String, Object> properties) {
super(properties);
}
}
|
Part.java
public class Part extends AbstractDocument implements HasType, HasModel, HasPrice {
public Part(Map<String, Object> properties) {
super(properties);
}
}
|
- Car는 이 패턴을 통해 어떤 장점을 얻었는가?
- Car라는 시스템은 속성의 변화에 매우 유연해졌다.
- Car에 새로운 Component로 Seat가 추가된다고 하면 Document의 sub interface로 HasSeat를 구현하고, 이를 Car에서 상속해주기만 하면 된다. 즉 어떤 속성이 추가 / 제거되더라도 Car에서 해당 Interface만 상속해주면 확장이 가능한 유연한 구조이다.
- Car에 가진 속성이 명시적으로 지정되었다
Compile Level에서 Car는 parts, price, model을 가지고 있고 이에 대한 전용 접근 메소드가 존재한다.
- 각 구현체가 Map<String, Object>를 가지고(아래처럼) get*를 통해 접근하는 것보다 나은 점은?
public class Car {
private Map<String, Object> properties = new HashMap<>();
public Stream<Part> getParts() { // some implemented code }
public int getPrice() { // some implemented code }
//… some property getter implementation
}
public class Part {
private Map<String, Object> properties = new HashMap<>();
public int getPrice() { // some implemented code }
public Type getType() { // some implemented code }
//.. some property getter implementation
}
|
AbstractDocumentPattern은 각 속성에 접근할 수 있는 getter를 따로 interface를 분리했는데 이를 통해 위 코드보다는 중복코드를 제거할 수 있다. Part, Car 모두 price 속성을 가지고 있으며 이러한 속성들은 각 interface에서 구현이 되어 있다. 최종 구현 클래스에서는 단순히 해당 treat interface를 상속만 하면 된다.
- childeren?
어떨 때 쓰는가?
- 강타입 언어(Java, C …)에서 무타입 언어(e.g javascript)의 유연성을 얻으면서도 타입 안전성을 확보하기 위해 쓴다.
- 다양한 속성을 가진 시스템을 유연성과 함께 타입 안전성을 어느정도 보장하며, 지속적으로 다양한 속성을 추가할 수 있도록 하기 위해 적용할 수 있는 패턴이다.
- 최종 구현체(e.g Car)에서는 Object get()을 쓰도록 설계되지 않았다. 언어적인 제약으로 최종 구현체에서 compile level에서 get이 hide되면 더 좋을거란 생각이 든다.
- put메소드를 통해 put할 경우 각 treat interface에서 예상치 못한 type의 값이 들어갈 수 있으므로, treat interface에 setter를 type 한정된 값으로 만드는 것도 좋은 방법이 될 수 있다.
EmoticonEmoticon