앞서서 이미 Factory Method라는 디자인 패턴에 대해서 다루었었다. 인스턴스를 생성해내는 추상적인 객체를 만들어두고, 구체적으로 어떤 인스턴스를 어떻게 만들지는 하위에서 구체적인 객체를 작성하면서 결정하는 디자인 패턴이었다.
Abstract Factory도 유사하다. 하지만 다른 것이라면 Abstract Factory의 목적은 하나의 "인스턴스"를 어떻게 만들지가 아닌 "비슷한 유형"의 객체들을 어떻게 만들어낼지에 대한 디자인 패턴이다.
그래서 추상적인 클래스들의 인터페이스들을 이용해 어떤식으로 조립이되는지에 대한 과정을 그려놓고, 어떤 제품을, 즉 결과물을 얻을지는 나중에 정의한 구체적인 객체들과 공장에 전달되는 매개변수에 맡겨놓는다.
Abstract Factory는 테마나 스킨처럼 기능과 속성이 유사한 제품군을 만들어야할 때 사용할 수 있다. 이때, Abstract Factory패턴을 적용함으로서 다음과 같은 이점을 가져갈 수 있다.
하지만, Abtract Factory는 제품의 조립과정이 이미 정의되어 있기 때문에 중간에 제품을 조립하기 위한 부품을 추가하거나 변경하는 것은 어려울 수 있다는 점에 주의하자.
햄버거와 피자를 파는 가게에서 주문을 받아 주문서를 작성하고 주문내역을 출력하는 프로그램을 Abstract Factory 패턴을 이용해 만들어보자.
Abstract Factory에는 등장하는 객체가 꽤 다양하고, 또 다양한만큼 복잡하게 여겨지기도 한다. 하지만, Abstract Factory가 무엇을 하려하는 디자인 패턴인지를 인지하면 객체는 많을지언정 각각의 역할은 결국 크게 다음의 네가지로 좁혀질 수 있다.
AbstractProduct 추상적인 제품
AbstractFactory에 의해 만들어질 추상적인 제품 혹은 제품을 구성하는 부품의 인터페이스를 미리 정의
AbstractFactory 추상적인 공장
AbstractProduct의 인스턴스를 만들어주기 위한 일련의 비즈니스 로직을 담은 인터페이스를 결정
ConcreteProduct 구체적인 제품
AbstractProduct의 인터페이스를 구현
ConcreteFactory 구체적인 공장
AbstractFactory의 인터페이스를 구현
Client 의뢰자
AbstractFactory와 AbstractProduct에서 정의한 인터페이스를 사용하여 제품을 만들어내고 사용
예제를 위해 비교적 많은 객체가 등장하기 때문에 그 이전에 앞서 살펴본 각 역할에 따라 필수객체만으로 간단하게 구조만 먼저 훑어보자.
interface AbstractFactory {
createProductA(): AbstractProductA;
createProductB(): AbstractProductB;
}
class ConcreteFactory implements AbstractFactory {
createProductA(): AbstractProductA {
return new ConcreteProductA();
}
createProductB(): AbstractProductB {
return new ConcreteProductB();
}
}
interface AbstractProductA {}
interface AbstractProductB {}
class ConcreteProductA implements AbstractProductA {}
class ConcreteProductB implements AbstractProductB {}
앞서 이야기한 것처럼 AbstractFactory 패턴의 목적은 "비슷한 유형"의 객체를 만들어내는데 있다. 따라서, ConcreteFactory
에서 ConcreteProduct
를 만들어주며, 이때 Template Method, Factory Method 같은 다른 디자인 패턴이 함께 접목되어 인스턴스를 어떻게 만들어줄지에 대해서도 인터페이스로 미리 정의해둘 수 있을 것이다.
이제 진짜 예제 코드를 작성해보자.
우선 각 메뉴들의 공통된 속성을 나타내는 Item
이라는 객체와, 각 메인메뉴 Main
과 음료 Drink
, 그리고 주문서 Order
의 인터페이스를 정의한다.
interface Item {
price: number,
info(): string;
}
abstract class Main implements Item {
constructor(
public price: number,
protected options: string,
) {}
abstract info(): string;
}
abstract class Drink implements Item {
constructor(
public price: number,
protected size: string,
) {}
abstract info(): string;
}
abstract class Order {
protected items: Item[] = [];
protected total: number = 0;
add(item: Item) {
this.items.push(item)
}
getTotal(): number {
this.total = 0;
this.items.forEach(item => {
this.total += item.price;
})
return this.total
}
abstract print(): void;
}
처음부터 갑자기 네 가지 객체를 AbstractProduct으로서 만들었지만, 이들은 결국 Order라는 완성된 제품을 만들어내기 위한 부품들에 해당할 뿐이다.
Item
에 해당하는 Main
과 Drink
가 있고, 이들은 Order
에 add
메소드를 이용해서 추가가 된다.
앞에서 만들어낸 Order
를 찍어낼 수 있는 OrderFactory
의 인터페이스를 만들자.
abstract class OrderFactory {
static getFactory(menu: 'burger'|'pizza'): OrderFactory {
switch (menu) {
case 'burger':
return new BurgerFactory();
case 'pizza':
return new PizzaFactory();
default:
throw new Error('Invalid menu');
}
}
abstract createMain(): Main
abstract createDrink(): Drink
abstract createOrder(): Order
}
OrderFactory
의 역할은 단순히 어떤 제품의 인스턴스를 만들어낼지만 결정하며, 그 밖의 해당 인스턴스로 수행할 수 있는 작업들이 함께 정의될 수 있다.
이 예제에서는 주문서에 메뉴를 추가할 때 Order
에 add
메소드를 사용하도록 하면서 createMain
과 같은 메소드들이 생성된 각 메뉴를 리턴하도록 했다. 하지만, 여기서 Builder
패턴도 접목시킬 수도 있을 것이다.
이제 각 인터페이스들에 구체적인 코드를 구현할 차례다.
// Burger
class Burger extends Main {
info(): string {
return `Burger with ${this.options}`
}
}
class SoftDrink extends Drink {
info(): string {
return `Soft drink with ${this.size}`
}
}
class BurgerOrder extends Order {
print(): void {
console.log('Burger order:');
this.items.forEach(item => {
console.log(item.info());
})
console.log(`Total: ${this.getTotal()}`);
}
}
// Pizza
class Pizza extends Main {
info(): string {
return `Pizza with ${this.options}`
}
}
class Beer extends Drink {
info(): string {
return `Beer with ${this.size}`
}
}
class PizzaOrder extends Order {
print(): void {
console.log('Pizza order:');
this.items.forEach(item => {
console.log(item.info());
})
console.log(`Total: ${this.getTotal()}`);
}
}
마지막으로 Pizza
와 Hamburger
를 만들어내는 ConcreteFactory를 구현한다.
class BurgerFactory extends Factory {
createMain() {
return new Burger(100, 'cheese')
}
createDrink() {
return new SoftDrink(50, 'large')
}
createOrder() {
return new BurgerOrder();
}
}
class PizzaFactory extends Factory {
createMain() {
return new Pizza(200, 'pepperoni')
}
createDrink() {
return new Beer(100, 'small')
}
createOrder() {
return new PizzaOrder();
}
}
완성된 코드를 실행해보자.
Abstract Factory 패턴에서는 실제 제품을 만들어내고 사용하는 단의 코드에서는 각 제품의 클래스를 직접 인스턴스화하거나 사용할 필요가 없어진다는 것에 주목하면서 보자.
const burgerFactory = Factory.getFactory('burger');
const burger = burgerFactory.createMain();
const drink = burgerFactory.createDrink();
const burgerOrder = burgerFactory.createOrder();
burgerOrder.add(burger);
burgerOrder.add(drink);
burgerOrder.print();
const pizzaFactory = Factory.getFactory('pizza');
const pizza = pizzaFactory.createMain();
const beer = pizzaFactory.createDrink();
const pizzaOrder = pizzaFactory.createOrder();
pizzaOrder.add(pizza);
pizzaOrder.add(beer);
pizzaOrder.print();
Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
sbcode.net - Abstract Factory Design Pattern