open:builder-pattern

Builder Pattern

턱 브라스(Tuck Brass)는 자동 커피 메이커 시스템이 오래돼서 너무 느리다고 불평한다. 고객들은 기다리지 못해 그냥 가버린다.

페드로: 무엇이 문제인지 정확히 이해할 필요가 있지 않나요?

이브: 연구해 봤는데, 시스템이 낡았고요, 코볼로 작성된 질의-응답 전문가 시스템으로 구축되어 있어요. 그것은 예전에는 아주 인기가 있었죠.

페드로: “질의-응답”이 무슨 말이에요?

이브: 터미널 앞에 사람이 있어요. 시스템이 “물을 추가할까요?” 라고 물으면, 사람이 “네”라고 답해요. 그러면 시스템이 다시 “커피를 추가할까요?” 라고 물으면, 사람이 “네”라고 답하죠. 뭐 이런 식이죠.

페드로: 악몽이로군요, 난 단지 밀크 커피를 원할 뿐인데요. 왜 미리 준비된 커피를 사용하지 않죠: 밀크 커피, 설탕 커피 등등.

이브: 컴퓨터가 재료를 혼합해서 모든 커피를 만들 수 있기를 원했던 거죠.

페드로: 오케이, 알겠어요. 빌더 패턴으로 고쳐봅시다.

public class Coffee {
  private String coffeeName; // required
  private double amountOfCoffee; // required
  private double water; // required
  private double milk; // optional
  private double sugar; // optional
  private double cinnamon; // optional

  private Coffee() { }

  public static class Builder {
    private String builderCoffeeName;
    private double builderAmountOfCoffee; // required
    private double builderWater; // required
    private double builderMilk; // optional
    private double builderSugar; // optional
    private double builderCinnamon; // optional

    public Builder() { }

    public Builder setCoffeeName(String name) {
      this.builderCoffeeName = name;
      return this;
    }

    public Builder setCoffee(double coffee) {
      this.builderAmountOfCoffee = coffee;
      return this;
    }

    public Builder setWater(double water) {
      this.builderWater = water;
      return this;
    }

    public Builder setMilk(double milk) {
      this.builderMilk = milk;
      return this;
    }

    public Builder setSugar(double sugar) {
      this.builderSugar = sugar;
      return this;
    }

    public Builder setCinnamon(double cinnamon) {
      this.builderCinnamon = cinnamon;
      return this;
    }

    public Coffee make() {
      Coffee c = new Coffee();
        c.coffeeName = builderCoffeeName;
        c.amountOfCoffee = builderAmountOfCoffee;
        c.water = builderWater;
        c.milk = builderMilk;
        c.sugar = builderSugar;
        c.cinnamon = builderCinnamon;

      // check required parameters and invariants
      if (c.coffeeName == null || c.coffeeName.equals("") ||
          c.amountOfCoffee <= 0 || c.water <= 0) {
        throw new IllegalArgumentException("Provide required parameters");
      }

      return c;
    }
  }
}

페드로: 보다시피, 커피 클래스의 인스턴스를 만드는 것이 쉽지 않아요. 내포된 Builder 클래스로 파라미터를 설정할 필요가 있죠.

Coffee c = new Coffee.Builder()
        .setCoffeeName("Royale Coffee")
        .setCoffee(15)
        .setWater(100)
        .setMilk(10)
        .setCinnamon(3)
        .make();

페드로: 메소드를 호출하면 모든 필수 파라미터를 검사를 하는데, 검사하다가 객체의 상태에 뭔가 문제가 있으면 예외를 던지죠.

이브: 멋진 기능이긴 한데, 상당히 장황하네요.

페드로: 클로저로 한 번 해보시죠.

이브: 식은 죽 먹기죠, 클로저는 선택 파라미터를 제공하는데, 그게 빌더 패턴을 대신할 수 있어요.

(defn make-coffee [name amount water
                   & {:keys [milk sugar cinnamon]
                      :or {milk 0 sugar 0 cinnamon 0}}]
  ;; definition goes here
  )

(make-coffee "Royale Coffee" 15 100
             :milk 10
             :cinnamon 3)

페드로: 아하, 파라미터 3개는 필수이고 나머지는 선택 파라미터들이긴 한데, 필수 파라미터에는 이름이 없군요.

이브: 무슨 말이죠?

페드로: 함수 호출할 때 숫자 15를 넘기는데, 그게 무엇을 의미하는지 전혀 모르쟎아요.

이브: 맞네요. 그러면 모든 파라미터에 이름을 짓고, 사전 조건(precondition)을 걸어 보죠. 그럼 당신이 한 것과 똑같죠.

(defn make-coffee
  [& {:keys [name amount water milk sugar cinnamon]
      :or {name "" amount 0 water 0 milk 0 sugar 0 cinnamon 0}}]
  {:pre [(not (empty? name))
         (> amount 0)
         (> water 0)]}
  ;; definition goes here
  )

(make-coffee :name "Royale Coffee"
             :amount 15
             :water 100
             :milk 10
             :cinnamon 3)

이브: 보다시피 모든 파라미터에 이름이 있고, 필수 파라미터는 :pre 조건을 주어 검사되고 있죠. 조건이 위반되면 AssertionError가 발생하죠.

페드로: 재밌네요. :pre는 언어의 일부인가요?

이브: 그렇죠. 단지 간단한 어써션(assertion)이에요. :post 조건도 있는데, 비슷해요.

페드로: 흠, 오케이. 하지만 알다시피 빌더 패턴은 가변 자료구조에 자주 사용되죠. 예를 들어, StringBuilder가 있어요.

이브: 가변 데이타를 사용하는 것은 클로저의 철학과는 맞지 않지만, 굳이 가변 데이타를 사용해야 한다해도, 문제는 없어요. deftype으로 클래스를 만든 다음, 변경하려는 속성에 일시적 가변 변수를 사용하면 돼요.

페드로: 코드를 보여주시죠.

이브: 가변 StringBuilder를 클로저로 구현한 예제가 여기 있어요. 단점도 있고 제한적이지만, 아이디어를 얻을 수 있어요.

;; interface
(defprotocol IStringBuilder
  (append [this s])
  (to-string [this]))

;; implementation
(deftype ClojureStringBuilder [charray ^:volatile-mutable last-pos]
  IStringBuilder
  (append [this s]
    (let [cs (char-array s)]
      (doseq [i (range (count cs))]
        (aset charray (+ last-pos i) (aget cs i))))
    (set! last-pos (+ last-pos (count s))))
  (to-string [this] (apply str (take last-pos charray))))

;; clojure binding
(defn new-string-builder []
  (ClojureStringBuilder. (char-array 100) 0))

;; usage
(def sb (new-string-builder))
(append sb "Toby Wong")
(to-string sb) => "Toby Wong"
(append sb " ")
(append sb "Toby Chung") => "Toby Wang Toby Chung"

페드로: 생각만큼 어렵지는 않네요.


  • open/builder-pattern.txt
  • 마지막으로 수정됨: 2021/11/21 13:32
  • 저자 127.0.0.1