생성 패턴
일반적으로 객체를 생성할 때에는 생성자(constructor)를 제공한다. 때로는 변형이 필요한데, 생성 객체에 초기값을 줄 때나 생성할 클래스를 선택하려 할 때가 그렇다.
여러 개의 생성자가 필요할 때면, 서로 협력이 필요하다.상속된 부모클래스와 자식클래스가 있을 때, 부모 클래스에 생성자를 추가했는데 자식 클래스에 생성자를 정의하지 않으면 오류가 난다.
요약하면, 객체를 만들기 위해 규칙을 가진 메소드인 생성자를 이용하는데, 일반적인 생성자가 제공하는 것 이상이 필요한 경우가 있다. 예를들어 파라미터에 초기값을 부여한다거나 객체 생성 전에 준비해야 한다거나의 경우이다. 따라서 객체 생성을 클래스 로직 밖으로 옮길 필요가 생겼다.
이에 생성패턴들이 생겨났다.
- 빌더 패턴
- 팩토리 메소드 패턴
- 추상 팩토리 패턴
서로 연관된 복잡한 생성자를 가지고 있을 때
// example
class Reservation {
public Reservation(int, String, String);
public Reservation(String, String);
public Reservation(int, String, String, int);
}
모든 데이터가 한꺼번에 제공되지 못할 때
생성자나 비즈니스룰이 복잡할 때
객체를 생성하기 전에 점진적으로 객체의 정보를 수집한다
Parser가 발견한 속성을 저장한 후, Reservation 객체를 구축한다. 생성자는 protected로 구현한다.
Reservation을 단순화하여 다른 작업에 더 집중하도록 하고, Scanner를 사용 점진적으로 생성하며, 예외를 생성할 수 있기 때문에 빌더를 사용한다.
객체 생성 로직을 클래스 밖으로 옮겨 한번에 만들지 않고 점진적으로 완성한다.
"new"를 사용할 때마다 캡슐화를 위배한다.
Duck duck = new DecoyDuck();
특정 서브타입을 인스턴스로 만드는 코드는 구체적인 클래스에 의존하게 된다.
나중에 무언가 변화가 생기면 코드를 다시 확인하고 추가하거나 제거해야 하는 번거로움이 생긴다.
이 문제를 해결하기 위해 인터페이스에 맞춰서 코딩을 한다면, 다형성 덕분에 시스템에서 일어날 수 있는 여러 변화에 대처할 수 있게 된다. 왜냐하면 구현 해야 하는 클래스에 implements를 해주면 사용할 수 있기 때문이다.
근데, 이 말은 곧 코드에서 구상 클래스(여기서는 인터페이스를 구현하는 클래스)들이 많아지면 새로운 구상 클래스를 추가할 때 마다 코드를 고쳐야 하기 때문에 문제가 발생한다. 결국 OCP 원리에 위배되는 것이다.
부모(상위) 클래스에 알려지지 않은 구체 클래스를 생성하고, 자식(하위) 클래스가 어떤 객체를 생성할지를 결정하도록 한다.
객체를 생성하는 인터페이스를 정의한다.
어떤 클래스를 객체화할지는 서브클래스가 결정한다.
인스턴스화를 서브클래스에게 미룬다.
피자 스토어
피자 스토어의 과정에는 1 만드는 과정과 2 준비하고 배달하는 과정이 있다
이를 코드화하면 아래처럼 만들 수 있음
public class PizzaStore {
Pizza orderPizza(String type){
Pizza pizza;
// Creation: 변동이 있을 부분
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("greek")) {
pizza = new GreekPizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
}
// Preperation: 고정된 부분
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
팩토리 메소드를 사용한다면
public abstract class PizzaStore {
// 팩토리 메소드
abstract Pizza createPizza(String item);
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
System.out.println("--- Making a "+pizza.getName()+" ---");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
public class NYPizzaStore extends PizzaStore {
Pizza createPizza(String item) {
if (type.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (type.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (type.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}
NYStyle을 따로 만들어서 서브클래스에 맞는 스타일의 피자를 만들 수 있게 되었다
만약, NYStyle 말고 다른 스타일이 필요하다면 새 서부클래스를 생성한다.
public class ChicagoPizzaStore extends PizzaStore {
Pizza createPizza(String item) {
if (type.equals("cheese")) {
return new ChicagoStyleCheesePizza();
} else if (type.equals("veggie")) {
return new ChicagoStyleVeggiePizza();
} else if (type.equals("pepperoni")) {
return new ChicagoStylePepperoniPizza();
} else return null;
}
public class ChicagoStylePepperoniPizza extends Pizza {
public ChicagoStylePepperoniPizza(){
name = "Chicago style Pepperoni Pizza";
dough = "Extra Thick Crust Dough";
sauce = "Plum Tomato Sauce";
toppings.add("Shredded Mozzarella Cheese");
toppings.add("Black Olives");
toppings.add("Spinach");
toppings.add("Eggplant");
toppings.add("Sliced Pepperoni");
}
void cut(){
System.out.println("cutting the pizza into square slices");
}
}
팩토리 메소드 패턴을 사용하여 여러 종류의 프랜차이즈를 쉽고 빠르게 생성할 수 있게 했다.
공통의 특징을 가지고 있는 객체의 패밀리를 생성
패밀리를 생성하기 위해 팩토리 인터페이스를 만든다.
추상 팩토리는 프로덕트의 패밀리를 생성하기 위한 인터페이스를 제공한다.
클라이언트 코드는 지역에 맞는 팩토리를 선택하고, 플러그인하고, 알맞은 피자 스타일을 올바른 재료의 세드로 가져온다.
관련된 의존 객체의 패밀리를 구체적인 클래스를 지정하지 않고 생성하기 위한 인터페이스를 제공한다.
구체적 클래스를 고립화한다
프로덕트의 패밀리를 쉽게 교환할 수 있다
프로덕트 사이의 일관성을 증진한다.
팩토리 메소드 패턴의 예시를 이어서 생각해보겠다.
어떤 프랜차이즈는 피자 스토어의 추상코드에 있는 절차를 따르되, 재료를 생략해 저가에 팔거나 이윤을 남기고 싶을 때 팩토리 메소드로는 충분하지 않기에 설계를 변경해야 한다.
피자 만드는 프로세스에서 재료를 준비하는 데 사용되는 팩토리를 제공해야한다.
다른 지역에 다른 재료를 사용하므로 ingredient 팩토리에 대한 지역 특수 서브클래스를 만들어야 한다.
더불어 모든 프랜차이즈에서 사용되는 표준도 맞추어야 한다.
이를 만족하는 추상팩토리 패턴을 적용한 피자 스토어의 클래스 다이어그램은 다음과 같다