추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.
추상팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이를 통해서 일련의 제품들을 공급받을 수 있다. (ex, createSauce())
이때, 실제로 어떤 제품이 생산되는지는 전혀 알 필요가 없다.
PizzaStore 디자인이 이제 슬슬 모양새를 갖춰가고 있다. 유연한 프레임워크(팩토리 메서드를 이용해)도 만들어졌고, 디자인 원칙도 충실하게 지키고 있다. 하지만 문제가 또 발생했다. 새로운 프레임워크가 도입된 이후로 몇몇 분점에서 자잘한 재료를 더 싼 재료로 바꿔서 원가를 줄이고 마진을 올린다는 것이다. 이런 일이 계속 되면 피자 브랜드에 타격이 올 수도 있다.
=> 해결책은 원재료를 생산하는 공장(팩토리)를 만들고 분점까지 재료를 배달하면 된다.
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClams();
}
지역별(뉴욕, 시카고 등) 팩토리를 만든다. 각 생성 메서드(createDough, createSauce 등)를 구현하는 클래스를 만든다.
ReggianoCheese, RedPeppers, ThickCrustDough와 같이 팩토리에서 사용할 원재료 클래스들을 구현한다. 상황에 따라 서로 다른 지역에서 같은 재료 클래스를 쓸 수도 있다.
그리고 나서 새로 만든 원재료 공장을 PizzaStore 코드에서 사용하도록 함으로써 모든 것을 하나로 묶어줘야 된다.
원재료 공장이 하나의 팩토리 객체라 보면 되고, 구체적인 객체(재료)는 구상 클래스(뉴욕 원재료 공장, 시카고 원재료 공장)에서 결정된다.
public class NYPizzaIngredientFactory implements PizzaIngredientFactory{
@Override
public Cheese createCheese() {
return new ReggianoCheese(); // 뉴욕식 피자 치즈: Reggiano 치즈
}
@Override
public Dough createDough() {
return new ThinCrustDough(); // 뉴욕식 피자 도우: 얇은 크러스트 도우
}
@Override
public Clams createClams() {
return new FreshClams(); // 뉴욕식 피자 조개: 생 조개
}
@Override
public Pepperoni createPepperoni() {
return new SlicedPepperoni(); // 뉴욕식 피자 페퍼로니: 슬라이스드 페퍼로니
}
@Override
public Sauce createSauce() {
return new MarinaraSauce(); // 뉴욕식 피자 소스: 마이나라 소스
}
@Override
public Veggies[] createVeggies() {
Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper()};
return veggies; // 뉴욕식 피자의 채료 재소들
}
}
원재료 팩토리 준비가 끝나고 이제 재료를 생산할 준비는 마쳤다. Pizza 클래스에서 팩토리에서 생산한 원재료만 사용하도록 코드를 고친다.
public abstract class Pizza {
String name;
String dough;
String sauce;
ArrayList toppings = new ArrayList();
void prepare() {
System.out.println("Preparing " + name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings: ");
for (int i = 0; i < toppings.size(); i++) {
System.out.println(" " + toppings.get(i));
}
}
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
...
...
}
// 원재료를 임의로 지정할 수 있다.
public class NYStylePepperoniPizza extends Pizza {
public NYStylePepperoniPizza() {
name = "NY Style Pepperoni Pizza";
dough = "Thin Crust Dough";
sauce = "Marinara Sauce";
toppings.add("Grated Reggiano Cheese");
toppings.add("Sliced Pepperoni");
toppings.add("Garlic");
toppings.add("Onion");
toppings.add("Mushrooms");
toppings.add("Red Pepper");
}
}
prepare() 메서드를 추상 메서드로 만든다. 이 부분에서 피자를 만드는 데 필요한 재료들을 정돈하게 된다. 물론 모든 원재료 팩토리에서 가져온다.
prepare() 메서드를 제외한 다른 메서드들은 바뀌지 않는다.
public abstract class Pizza {
String name;
Dough dough;
Sauce sauce;
Veggies veggies[];
Cheese cheese;
Pepperoni pepperoni;
Clams clams;
// 추상 클래스로..
abstract void prepare();
void bake(){
System.out.println("Bake for 25 minutes at 350");
}
...
...
}
Pizza 추상 클래스가 준비되었으면 뉴욕풍 피자, 시카고 풍 피자를 만들어야 한다. 달라진 점은 원재료 공장에서 바로 가져온다는 것 밖에 없다. 하지만 분점에서 마음대로 재료를 사용하는 것은 이제부터 불가능하다.
팩토리 메서드 패턴을 이용한 코드에서는 NYCheesePizza와 ChicagoCheesePizza 클래스를 만들었다. 이 두 클래스는 지역별로 다른 재료를 사용한다는 것만 빼면 다른 점이 없었다. 또한 야채 피자나 조개 피자도 마찬가지이다. 재료만 다를 뿐 준비 단계들은 똑같다.
팩토리 메서드 패턴에서의 피자 구상 클래스들.
// 뉴욕 풍 치즈피자 public class NYStyleCheesePizza extends Pizza { public NYStyleCheesePizza() { name = "NY Style Sauce and Cheese Pizza"; dough = "Thin Crust Dough"; sauce = "Marinara Sauce"; toppings.add("Grated Reggiano Cheese"); } }
// 시카고 풍 치즈피자 public class ChicagoStyleCheesePizza extends Pizza { public ChicagoStyleCheesePizza() { name = "Chicago Style Deep Dish Cheese Pizza"; dough = "Extra Thick Crust Dough"; sauce = "Plum Tomato Sauce"; toppings.add("Shredded Mozzarella Cheese"); } // 시카고 피자는 조금 다르게 잘라야 하므로 cut() 메서드를 오버라이딩한다. void cut() { System.out.println("Cutting the pizza into square slices"); } }
// 뉴욕 풍 야채 피자와 조개 피자 public class NYStyleVeggiePizza extends Pizza { public NYStyleVeggiePizza() { name = "NY Style Veggie Pizza"; dough = "Thin Crust Dough"; sauce = "Marinara Sauce"; toppings.add("Grated Reggiano Cheese"); toppings.add("Garlic"); toppings.add("Onion"); toppings.add("Mushrooms"); toppings.add("Red Pepper"); } } public class NYStyleClamPizza extends Pizza { public NYStyleClamPizza() { name = "NY Style Clam Pizza"; dough = "Thin Crust Dough"; sauce = "Marinara Sauce"; toppings.add("Grated Reggiano Cheese"); toppings.add("Fresh Clams from Long Island Sound"); } }
따라서 피자마다 클래스를 지역별로 따로 만들 필요가 없다는 결론을 내릴 수 있다. 지역별로 다른 점은 원재료 공장에서 커버해준다.
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
/*
피자를 만들기 위해 원재료를 제공하는 공장이 필요.
따라서 각 피자 클래스에서는 생성자를 통해서 팩토리를 전달받는다.
이 팩토리는 인스턴스 변수에 저장하면 된다.
*/
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
/*
prepare()는 팩토리가 작동하는 부분이다.
치즈 피자를 만들기 위한 각 단계를 처리한다.
재료가 필요할 때마다 팩토리에 있는 메서드를 호출해서 만들어 온다.
*/
void prepare() {
System.out.println("Preparing " + name);
dough = ingredientFactory.createDough(); // 뉴욕식이라면 크러스트 도우가
sauce = ingredientFactory.createSauce(); // 뉴욕식이라면 마리나라 소스가
cheese = ingredientFactory.createCheese(); // 뉴욕식이라면 Reggiano 치즈가
// 치즈피자에는 Clam(조개), 페퍼로니, 야채 재료들이 필요없다.
}
}
public class ClamPizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
public ClamPizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
System.out.println("Preparing " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
// 조개 피자에는 조개 재료가 필요하다.
clam = ingredientFactory.createClam(); // 뉴욕식이라면 freshClam이 재료로 들어감.
}
}
sauce = ingredientFactory.createSauce();
1) Pizza에 있는 인스턴스 변수에 이 피자에서 사용할 특정 소스에 대한 레퍼런스를 대입한다.
2) ingredientFactory <= 사용할 원재료 팩토리. Pizza 클래스에서는 원재료 팩토리가 맞기만 하다면 어떤 팩토리를 쓰든 상관이 없다.
3) .createSauce(); <= 해당 지역에서 쓰이는 소스를 리턴한다. 뉴욕 공장이라면 마리나라 소스를 리턴할 것이다.
public class NYPizzaStore extends PizzaStore {
Pizza createPizza(String item) {
if (item.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (item.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (item.equals("clam")) {
return new NYStyleClamPizza();
} else if (item.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory =
new NYPizzaIngredientFactory();
if (item.equals("cheese")) {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("New York Style Veggie Pizza");
} else if (item.equals("clam")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza");
}
return pizza;
}
}
뉴욕 가게에서 치즈 피자를 주문한다.
// in Main
PizzaStore = nyPizzaStore = new NYPizzaStore();
// in Main
nyPizzaStore.orderPizza("cheese");
// in PizzaStore 추상 클래스의 orderPizza() 메서드
Pizza pizza = createPizza("cheese");
// in NYPizzaStore 구상 클래스의 createPizza() 메서드
Pizza pizza = new CheesePizza(nyIngredientFactory);
// 그리고 생성된 피자 객체를 반환하여 다시 orderPizza() 메서드로 복귀
// orderPizza() 메서드에서는 반환받은 객체를 통해 prepare() 메서드를 호출한다.
// in CheesePizza 클래스의 prepare() 메서드 호출
void prepare(){
dough = factory.createDough();
..
..
}
추상 팩토리(Abstract Factory)라고 부르는 이 새로운 형식의 팩토리를 도입해서 서로 다른 피자에서 필요로 하는 원재료군을 생산하기 위한 방법을 구축하였다.
추상 팩토리(PizzaIngredientFactory)를 통해서 제품군을 생성하기 위한 인터페이스를 제공할 수 있다. 피자 가게 코드에서는 이 팩토리를 이용하기만 하면 된다. => 자신들이 원재료에 대한 고민을 하지 않아도 됨.