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