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"
페드로: 생각만큼 어렵지는 않네요.