독서/헤드퍼스트 디자인패턴

[헤드퍼스트 디자인 패턴] 4장: 팩토리 패턴

yesman9 2023. 9. 13. 11:46

아래 링크 Repository 중 내가 작성한 부분만 가져온 글이다.

https://github.com/yesman9692/Head-First-Design-Patterns/tree/main

4장: 팩토리 패턴

new 연산자의 문제점

변화에 취약하다!

Duck duck;
if(picnic){
    duck = new MallardDuck();
} else if(hunting){
    duck = new DecoyDuck();
} else if(inBathTub){
    duck = new RubberDuck();
}

위의 코드처럼 Duck이라는 인터페이스가 있지만 그럼에도 불구하고
MallardDuck, DecoyDuck, RubberDuck과 같은 인스턴스를 만들어야 한다.
코드를 변경하거나 확장시에는 기존 코드를 제거하거나 수정이 불가피하다.

피자 예제

  • 기존 방식대로 아래와 같이 분기에 따라 객체를 생성해보자
Pizza orderPizza(String type){
    Pizza pizza;

    if(type.equals("cheese")){
        pizza = new CheesePizza();
    } else if(type.equals("pepperoni")){
        pizza = new PepperoniPizza();
    } else if(type.equals("clam")){
        pizza = new ClamPizza();
    } else if(type.equals("veggie")){
        pizza = new VeggiePizza();
    }

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();

    return pizza;
}

문제점

  • 새로운 피자가 추가되면 코드를 수정(추가)해야한다.
  • 메뉴가 없어지만 코드를 수정(삭제)해야한다.

객체 생성 부분 캡슐화하기

  • 객체 생성 부분을 따로 빼자
  • 이걸 우린 팩토리라고 부르기로 했어요
public class SimplePizzaFactory{
    public Pizza CreatePizza(String type){
        Pizza pizza = null;

        if(type.equals("cheese")){
            pizza = new CheesePizza();
        } else if(type.equals("pepperoni")){
            pizza = new PepperoniPizza();
        } else if(type.equals("clam")){
            pizza = new ClamPizza();
        } else if(type.equals("veggie")){
            pizza = new VeggiePizza();
        }
        return pizza;
    }
}

팩토리를 사용할 클라이언트를 만들자

public class PizzaStore{
    SimplePizzaFactory factory;

    public PizzaStore(SimplePizzaFactory factory){
        this.factory = factory;
    }

    public Pizza orderPizza(String type){
        Pizza pizza;

        pizza = factory.CreatePizza(type);

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}
메인문
public class Main {
    public static void main(String args[]) {
        SimplePizzaFactory simplePizzaFactory = new SimplePizzaFactory();
        PizzaStore pizzaStore = new PizzaStore(simplePizzaFactory);
        pizzaStore.orderPizza("pepperoni")
    }
}
  • 이제 치즈피자, 페퍼로니피자, 클램피자 각각의 피자를 생성할 때 new 키워드를 사용 할 필요가 없다간단한 팩토리를 이용해서 다양한 스타일의 피자를 만들어보자
  • 뉴욕 스타일 피자
  • 시카고 스타일 피자
    NYPizzaFactory nyFactory = new NYPizzaFactory();
    PizzaStore nyStore = new PizzaStore(nyFactory);
    nyStore.orderPizza("Veggie");
    

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");


### 간단한 팩토리
- 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝다
![screensh](https://velog.velcdn.com/images/jeong-god/post/3565f49d-bee3-47e4-b752-3f7296b9d4b2/image.png)

간단하지만, 유연성이 떨어진다. 아래 경우 매번 새로운 팩토리를 만들어야한다.
- 굽는 방식이 달라지거나
- 피자 모양이 다르거나
- 피자를 자르는 방식이 다르거나

## 팩토리 메소드
##### 피자가게(생산자) 클래스
```java
public abstract class PizzaStore{
    public Pizza orderPizza(String type){
        Pizza pizza;

        // createPizza 메소드를 팩토리 객체에서 사용하지 않고 직접 사용한다. 단, 추상메소드를 사용한다.
        pizza = createPizza(type); 

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
    protected abstract Pizza createPizza(String type);
}

public class NYPizzaStore extends PizzaStore{
    // 추상 메소드를 서브 클래스에서 구현한다. 서브클래스를 이용해 각각 다른 피자스타일을 만들 수 있다.
    Pizza createPizza(String item){
        if(type.equals("cheese")){
            return new NYStyleCheesePizza();
        } else if(type.equals("pepperoni")){
            return new NYStylePepperoniPizza();
        } else if(type.equals("clam")){
            return new NYStyleClamPizza();
        } else if(type.equals("veggie")){
            return new NYStyleVeggiePizza();
        }
    }
}
피자(제품) 클래스
public abstract class Pizza {
    String name;
    String dough;
    String sauce;
    List toppings = new ArrayList();
    void prepare() {
        System.out.println("준비 중:" + name);
        System.out.println("도우 돌리는 중:" + dough);
        System.out.println("소스 뿌리는 중:" + sauce);
        System.out.println("토핑을 올리는 중:" + toppings);
    }
    void bake() {
        System.out.println("굽는중");
    }
    void cut() {
        System.out.println("자르는중");
    }
    void box() {
        System.out.println("포장중");
    }
}

// Pizza클래스를 상속받아 제품(뉴욕치즈피자)을 구현한다
public class NYStyleCheesePizza extends Pizza {
    public NYStyleCheesPizz() {
        name = "뉴옥 스타일 소스와 치즈 피자";
        dough = "씬 크러스트 도우";
        sauce = "마리나라 소스"
    }
}

// Pizza클래스를 상속받아 제품(시카고치즈피자)을 구현한다
public class ChicagoStyleCheesePizza extends Pizza {
    public NYStyleCheesPizz() {
        name = "시카고 스타일 딥 디쉬 치즈 피자";
        dough = "아주 두꺼운 크러스트 도우";
        sauce = "플럼토마토 소스"
    }
}
메인문
public class PizzaTestDrive {
    public static void main(String[] args) {
        PizzStore nyStore = new NYPizzaStore();
        PizzStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizz("cheese"); // 뉴욕 치즈피자 생성

        Pizza pizza = chicagoStore.orderPizz("cheese"); // 시카고 치즈피자 생성
    }
}

팩토리 메소드 패턴 살펴보기

  • 객체를 생성할 때 필요한 인터페이스를 만듭니다.
  • 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다.
  • 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에 맡기게 됩니다.
    screensh

추상 팩토리 패턴

  • 그런데 같은 메뉴여도 지역마다 다른 재료를 사용한다면?
  • 원재료를 생산하는 팩토리를 만들면 편하다
    재료공장(생산자) 클래스
    // 원재료 팩토리 인터페이스
    public interface PizzaIngredientFactory {    
      public Dough createDough();    
      public Sauce createSauce();    
      public Cheese createCheese();    
      public Veggies[] createVeggies();    
      public Pepperoni createPepperoni();    
      public Clams createClams(); 
    }
    

// 뉴욕 피자의 재료를 생상하는 뉴욕 팩토리. 재료들은 여기서 구현한다.
public class NYPizzaingredientFactory implements PizzaIngredientFactory{
@Override
public Dough createDough() {
return new ThinCrustdough();
}

@Override    
public Sauce createSauce() {        
    return new MarinaraSauce();    
}    

@Override    
public Cheese createCheese() {        
    return new ReggianoCheese();    
}    

@Override    
public Veggies[] createVeggies() {        
    Veggies veggies[] = { new Farlic(), new Onion(), new Mushroom(), new RedPepper() };        
    return veggies;    
}    

@Override    
public Pepperoni createPepperoni() {        
    return new SlicedPepperoni();    
}    

@Override    
public Clams createClams() {        
    return new Freshclams();    
} 

}

##### 피자(제품) 클래스
- Pizza의 prepare()를 추상 메소드화 하고, 구상 클래스에서 원재료를 팩토리에서 바로 가져와 prepare()를 구현하면, 피자마다 클래스를 지역별로 따로 만들 필요가 없다.
```java
public abstract class Pizza{    
    String name;    

    Dough dough;    
    Sauce sauce;    
    Veggies veggies[];    
    Cheese cheese;    
    Pepperoni pepperoni;    
    Clams clams;        

    abstract void prepare(); //추상 메소드로 변경됨    

    //기타 메소드
}
public class CheesePizza extends Pizza{    
    PizzaIngredientFactory ingredientFactory;    

    public CheesePizza(PizzaIngredientFactory ingredientFactory) {        
        this.ingredientFactory = ingredientFactory;    
    }    

    @Override    
    public void prepare() {        
        this.dough = ingredientFactory.createDough();        
        this.sauce = ingredientFactory.createSauce();        
        this.cheese = ingredientFactory.createCheese();    
    } 
}
pizzaStore
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("뉴욕 스타일 치즈 피자")
        } else if(item.equals("veggie")) {
            pizza = new VeggiePizza(ingredientFactory);
            pizza.setName("뉴욕 스타일 야채 피자")
        } else if(item.equals("clam")) {
            pizza = new ClamPizza(ingredientFactory);
            pizza.setName("뉴욕 스타일 조개 피자")
        } else if(item.equals("pepperoni")) {
            pizza = new PepperoniPizza(ingredientFactory);
            pizza.setName("뉴욕 스타일 페퍼로니 피자")
        }
        return pizza;
    }
}
메인문
public class PizzaTestDrive {
    public static void main(String[] args) {
        PizzStore nyStore = new NYPizzaStore();
        PizzStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese"); // 뉴욕 치즈피자 생성

        Pizza pizza = chicagoStore.orderPizz("cheese"); // 시카고 치즈피자 생성
    }
}

팩토리 메소드 패턴 vs 추상 팩토리 패턴

  • 둘다 팩토리 객체를 통해 구체적인 타입을 감추고 객체 생성에 관여하는 패턴 임에는 동일하다. 또한 공장 클래스가 제품 클래스를 각각 나뉘어 느슨한 결합 구조를 구성하는 모습 역시 둘이 유사하다.
  • 그러나 주의할 것은 추상 팩토리 패턴이 팩토리 메서드 패턴의 상위 호환이 아니라는 점이다. 두 패턴의 차이는 명확하기 때문에 상황에 따라 적절한 선택을 해야 한다.
  • 예를들어 팩토리 메서드 패턴은 객체 생성 이후 해야 할 일의 공통점을 정의하는데 초점을 맞추는 반면, 추상 팩토리 패턴은 생성해야 할 객체 집합 군의 공통점에 초점을 맞춘다.
  • 이 둘을 유사점과 차이점을 조합해서 복합 패턴을 구성하는 것도 가능하다.
    screensh