2017년 12월 31일 일요일

AbstractDocument pattern

Tags

정의

무타입 언어의 유연성을 Java, C와 같은 타입형 언어에서 얻을 수 있도록 한 패턴이다.

구성요소

  • Document
Root Interface로 Document가 정의된다. 여기서 Document는 String, Number부터 다른 Document리스트까지 모든 타입값을 가질 수 있도록 한 인터페이스라고 볼 수 있다.
    • put
      • Document가 가진 속성을 추가/변경할 수 있는 메소드다.
    • get
      • Document가 가진 속성에 접근할 수 있도록 하는 메소드다
    • children
      • Document의 sub Document에 접근할 수 있도록 하는 메소드다.
      • sub document의 타입 안정성을 위해 factory function을 인자로 받는 것이 독특하다.

  • BaseDocument
Document interface의 추상클래스이며, 속성을 저장할 map을 field로 갖는다. 이 map은 어떤 타입의 데이터도 저장할 수 있는 형태의 타입이어야 한다.

  • Treat Interface(e.g Has* interface)
Document의 확장으로 고유 특성의 값에 접근할 수 있도록 한 interface이다. get*메소드를 통해 특정한 필드에 접근하도록 강제하여 이를 통해 타입안전성을 확보한다. 또한 세부 속성마다 interface가 존재하기때문에 구현 시스템이 선택적으로 유연하게 확장할 수 있다.

  • Implementation class(e.g Car)
    • Treat Interface를 선택적으로 구현한 클래스이다.
    • 가진 속성에 따라 Treat Interface를 선택적으로 구현한다.

예제 Car

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 한정된 값으로 만드는 것도 좋은 방법이 될 수 있다.

This Is The Newest Post


EmoticonEmoticon

:)
:(
hihi
:-)
:D
=D
:-d
;(
;-(
@-)
:o
:>)
(o)
:p
:-?
(p)
:-s
8-)
:-t
:-b
b-(
(y)
x-)
(h)