HeadFirst 04. 팩토리 패턴

Jiyeong·2022년 6월 18일
0

GoF

목록 보기
4/5

인터페이스 바탕으로 만들어진 코드는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기에 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있음. -> 다형성

최첨단 피자 코드 만들기

public orderPizza() {
	Pizza pizza = new Pizza();
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

피자 종류를 고르고 그에 맞게 피자를 만드는 코드 추가!

Pizza orderPizza(String type){ //type -> orderPizza 메소드 인자로 피자 종류 전달
	Pizza pizza;
    
    //피자 종류를 바탕으로 구상 클래스의 인스턴스를 만들고, 
    //pizza 인스턴스 변수에 그 인스턴스를 대입함. 
    //이 모든 피자 클래스는 pizza 인터페이스를 구현함.
    if(type.equals("cheese"){
    	pizza = new CheesePizza();
    } else if(type.equals("greek"){
    	pizza = new GreekPizza();
    } else if(type.equals("pepperoni"){
    	pizza = new PepperoniPizza();
    }
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

피자 코드 추가하기 / 객체 생성 부분 캡슐화

신메뉴 추가시 인스턴스를 만드는 구상 클래스를 선택해야 하는 부분이 문제!
-> 캡슐화하기

public class SimplePizzaFactory{
//이 클래스는 클라이언트가 받을 피자만 만듦

//createPizza() 메소드를 정의. 
	public Pizza createPizza(String type){
   		Pizza pizza = null;
        
      if(type.equals("cheese"){
          pizza = new CheesePizza();
      } else if(type.equals("clam"){
          pizza = new ClamPizza();
      } else if(type.equals("pepperoni"){
          pizza = new PepperoniPizza();
      } else if(type.equals("veggie"){
          pizza = new VeggiePizza();
      } 
    	return pizza;
    }
}

SimplePizzaFactoryㄹ르 사용하는 클라이언트가 매우 많을 수 있는데, 피자 객체를 받아 설명하거나 가격을 알려주는 PizzaShopMenu 클래스와 조금 다른 방식으로 피자 주문을 처리하는 HomeDelivery 클래스에도 이 팩토리를 사용할 수 있음.

이렇게 피자 객체 생성 작업을 팩토리 클래스로 캡슐화 해두면 구현을 할 때 팩토리 클래스 하나만 고치면 돼서 편리함.

클라이언트 코드 수정하기

public class PizzaStore {
//PizzaStore에 SimplePizzaFactory의 레퍼런스 저장
	SimplePizzaFactory factory;
    
    //PizzaStore의 생성자에 팩토리 객체가 전달됨
    public PizzaStore(SimplePizzaFactory factory) {
    	this.factory = factory;
    }
    
    public Pizza orderPizza(String type){
    	Pizza pizza;
        
        pizza = factory.createPizza(type);
        //new 연산자 대신 팩토리 객체에 있는 create 메소드를 사용
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
}

객체 구성을 활용하면 행동을 실행할 때 구현된 객체를 동적으로 바꿀 수 있음!

'간단한 팩토리'의 정의

Simple Factory는 디자인 패턴이라기 보단 프로그래밍에서 자주 쓰이는 관용구에 가깝다.

  1. PizzaStore > orderPizza()
    팩토리를 사용하는 클라이언트로 simplePizzaFactory로부터 피자 인스턴스를 받게 됨.

  2. SimplePizzaFactory > createPizza()
    피자 객체를 생성하는 팩토리로 이 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조하는 부분.
    createPizza()와 같은 메소드를 정적 메소드로 선언하는 경우가 종종 있음.

  3. Pizza > prepare(), bake(), cut(), box()
    팩토리에서 만드는 피자로 Pizza 클래스는 메소드를 오버라이드해서 쓸 수 있도록 추상 클래스를 정의함.

  4. 종류별 피자들
    팩토리에서 생산하는 제품에 해당하는 구상 클래스로 각 피자는
    1) Pizza 인터페이스를 구현해야 함
    2) 구상 클래스여야만 함

다양한 팩토리 만들기

피자 지점을 늘리기 위해 SimplePizzaFactory를 삭제하고, 3가지 서로 다른 팩토리를 만들고, PizzaStore에서 적당한 팩토리를 사용하도록 하는 방법.

피자 가게 프레임워크 만들기

createPizza() 메소드를 추상 메소드로 선언하고, 지역별 스타일에 맞게 PizzaStore의 서브클래스를 만듦.

public abstract class PizzaStore{
//PizzaStore는 추상 클래스가 됨.

	public Pizza orderPizza(String type){
    	Pizza pizza;
        
        pizza = createPizza(type);
        //팩토리 객체가 아닌 PizzaStore의 createPizza를 호출
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    abstract Pizza createPizza(String type);
    //팩토리 객체 대신 이 메소드를 사용함.
    //이제 팩토리 메소드가 PizzaStore의 추상 메소드로 바뀜.
}

피자의 종류는 어떤 서브클래스를 선택했느냐에 따라 결정됨.

피자 스타일 서브클래스 만들기

public class NYPizzaStore extends PizzaStore{
//orderPizza() 메소드도 자동으로 상속 받음.
//orderPizza()는 Pizza 객체를 리턴 
//Pizza의 서브클래스 가운데 어느 구상 클래스 객체의 인스턴스를 만들어서 리턴할 지는 전적으로 PizzaStore 서브클래스에 의해 결정됨.

//createPizza()는 PizzaStore에서 추상 메소드로 선언되었으므로 구상 클래스에서 반드시 구현
	Pizza createPizza(String item){
    	 if(type.equals("cheese"){
          return new NYStyleCheesePizza();
        } else if(type.equals("clam"){
          return new NYStyleClamPizza();
        } else if(type.equals("pepperoni"){
          return new NYStylePepperoniPizza();
        } else if(type.equals("veggie"){
          return new NYStyleVeggiePizza();
        } else return null;
        //구상 클래스의 객체를 생성. 피자 종류에 해당하는 뉴욕 스타일 피자를 생성해 리턴
    }
}

팩토리 메소드 선언하기

구상 클래스 인스턴스를 만드는 일을 하나의 객체가 전부 철하는 방식에서 일련의 서브 클래스가 처리하는 방식으로 변환.

public abstract class PizzaStore{
	
    //피자 객체 인스턴스를 만드는 일은 PizzaStore의 서브 클래스에 있는 createPizza() 메소드에서 처리함.
	public Pizza orderPizza(String type){
    	Pizza pizza;
        
        pizza = createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    protected abstract Pizza createPizza(String type);
    //Pizza 인스턴스를 만드는 일은 팩토리 메소드에서 맡아 처리함
}

팩토리 메소드는 객체 생성을 서브클래스에 캡슐화할 수 있음.
그러면 슈퍼클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리 가능.

abstract : 팩토리 메소드를 추상 메소드로 선언해 서브클래스가 객체 생성을 책임지도록 함.

Product : 팩토리 메소드는 특정 객체를 리턴, 그 객체는 보통 슈퍼클래스가 정의한 메소드 내에서 쓰임.

factoryMethod : 팩토리 메소드는 클라이언트(슈퍼클래스에 있는 orderPizza() 같은 코드)에서 실제로 생성되는 구성 객체가 무엇인지 알 수 있게 만드는 역할.

String type : 팩토리 메소드를 만들 때 매개변수로 만들 객체 종류를 선택 가능

Pizza 클래스 만들기

public abstract class Pizza{
	String name;
    String dough;
    String sauce;
    List<String> toppings = new ArrayList<String>();
    void prepare(){
        System.out.println("준비 중:" + name);
        System.out.println("도우를 돌리는 중...");
        System.out.println("소스를 뿌리는 중...");
        System.out.println("토핑을 올리는 중...");
        for (String topping : toppings) {
          System.out.println("" + topping);
        }
    }
    
	void bake(){
    	System.out.println("175도에서 25분간 굽기");
    }
    
    void cut(){
    	System.out.println("피자를 사선으로 자르기");
    }
    
    void box(){
    	System.out.println("상자에게 피자 담기");
    }
    
    public String getName(){
    	return name;
    } 
}

구상 서브클래스 만들기

public class NYStyleCheesePizza extends Pizza{
	public NYStyleCheesePizza(){
    	name = "뉴욕 스타일 소스와 치즈 피자";
        dough = "씬 크러스트 도우";
        sauce = "마리나라 소스";
        
        toppings.add("잘게 썬 레지아노 치즈");
    }
}

public class ChicagoStyleCheesePizza extends Pizza{
	public ChicagoStyleCheesePizza(){
    	name = "시카고 스타일 소스와 치즈 피자";
        dough = "아주 두꺼운 크러스트 도우";
        sauce = "플럼토마토 소스";
        
        toppings.add("잘게 썬 모짜렐라 치즈");
    }
    
        void cut(){
    	System.out.println("네모난 모양으로 피자 자르기");
    }
}

최첨단 피자 코드 테스트

public class PizzaTestDrive{
	public static void main(String args[]){
    	PizzaStore nyStore = new NYPizzaStore();
        PizzaStore chicagoStore = new ChicagoPizzaStore();
        
        Pizza pizza = nyStore.orderPizza("cheese");
        System.out.println("에단이 주문한"+pizza.getName()+"\n");
        
        Pizza pizza = chicagoStore.orderPizza("cheese");
        System.out.println("조엘이 주문한"+pizza.getName()+"\n");
    }
}

슈퍼클래스가 뭘 하지 않아도 서브클래스에서 알아서 해결해줌!

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

Creator(생산자) 클래스

PizzaStore > createPizza(), orderPizza()

  • 생산자 클래스에 추상 제품 클래스에 의존하는 코드가 들어있을 때도 있음. 이 제품 클래스의 객체는 클래스의 서브클래스에 의해 만들어지며, 생산자 자체는 어떤 구상 제품 클래스가 만들어질 지 미리 알 수 없음.
  • 추상 생산자 클래스로 나중에 서브 클래스에서 제품(객체)를 생산하려고 구현하는 팩토리 메소드(추상 메소드)를 정의함.

아래 두 개의 구상 생산자(concrete creator) 클래스는 제품을 생산.

NYPizzaStore > createPizza()
여기에선 orderPizza() 메소드가 팩토리 메소드로 객체를 생성함.

ChicagoPizzaStore > createPizza()
각 분점마다 PizzaStore의 서브클래스가 있으므로 createPizza() 메소드 구현을 활용해 그 가게의 고유 피자를 맘대로 만들 수 있음.

Product(제품) 클래스

Pizza
팩토리는 제품을 생산!
그 밑에 구상클래스로 다양한 종류의 피자들이 있음.

병렬 클래스 계층 구조 알아보기

팩토리 메소드는 이러한 방법을 캡슐화하는데 있어 가장 핵심적인 역할을 함!
~p.165

팩토리 메소드 패턴 정의

Factory Method Pattern은 객체를 생성할 때 필요한 인터페이스를 만듦. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정. 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에 맡기게 됨.

Creator 추상 클래스에서 객체를 만드는 메소드로 팩토리 메소드용 인터페이스를 제공한다는 사실을 알 수 있음. Creator 추상 클래스에 구현되어 있는 다른 메소드는 팩토리 메소드에 의해 생산된 제품으로 필요한 작업을 처리한다.
-> 실제 팩토리 메소드를 구현하고 제품(객체 인스턴스)을 만드는 일은 서브클래스에서만 가능

사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정된다

Creator : 제품으로 원하는 일을 할 때 필요한 모든 메소드가 구현되어 있음. 제품을 만들어 주는 팩토리 메소드는 추상 메소드가 정의되어 있을 뿐 구현되어 있지는 않음.

  • factoryMethod() : Creator의 모든 서브클래스에 factoryMethod() 추상 메소드를 구현해야 함.

ConcreteCreator : 실제로 제품을 생산하는 factoryMethod()를 구현
구상 클래스 인스턴스를 만드는 일은 ConcreteCreator가 책임진다. 실제 제품을 만드는 방법을 알고 있는 클래스는 이 클래스 뿐!

Product : 제품 클래스는 모두 똑같은 인터페이스를 구현해야 함. 그래야 그 제품을 사용한 클래스에서 구상클래스가 아닌 인터페이스의 레퍼런스로 객체를 참조 가능.

Q5. 매개변수 팩토리 메소드를 사용하면 형식 안전성(type-safety)에 지장이 있지 않나요? String을 매개변수로 전달할 뿐이니까요.
A5. 이런 경우 '런타임 에러'가 발생한다. 매개변수 팩토리 메소드를 사용할 때 형식 안전성을 조금 더 잘 보장해 줄 수 있는 기법이 있는데, 그런 기법을 사용하면 형식 오류를 컴파일 시에 잡아낼 수 있음.

Dependency Inversion Principle

추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다
고수준 구성요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다.

  • 변수에 구상 클래스의 레퍼런스를 저장하지 말 것
    new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 되기에 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지하기!

  • 구상 클래스에서 유도된 클래스를 만들지 말 것
    구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.

  • 베이스 클래스에서 이미 구현되어 있는 메소드를 오버라이드 하지 말 것
    이미 구현되어 있는 메소드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브 클래스에서 공유할 수 있는 것만 정의해야 한다.

원재료 팩토리 만들기

public interface PizzaIngredientFactory{
	
    public Dough createDough();
    public Sauce createSauce();
    public Cheese createCheese();
    public Veggies[] createVeggies();
    public Pepperoni createPepperoni();
    public Clams createClams();
}

TIP! 생각해보기!
1. 지역별로 팩토리를 만든다. 각 생성 메소드를 구현하는 PizzaIngredientFactory 클래스를 만들어야 한다.
2. ReggianoCheese, RedPeppers, ThickCrustDough와 같이 팩토리에서 사용할 원재료 클래스를 구현한다. 상황에 따라 서로 다른 지역에서 같은 재료 클래스를 쓸 수도 있다.
3. 그리고 나서 새로 만든 원재료 팩토리를 PizzaStore 코드에서 사용하도록 모든 것을 하나로 묶어야 함!

뉴욕 원재료 팩토리 만들기

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
  //모든 재료 공장에서 구현해야 하는 인터페이스를 뉴욕 원재료 팩토리에서도 구현해야 함.
  
  public Dough createDough() {
    return new ThinCrustDough();
  }

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

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

  public Veggies[] createVeggiese() {
    Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
    return veggies;
  }

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

  public Clams createClam() {
    return new FreshClams();
  }
}

Pizza 클래스 변경하기

public abstract class Pizza {
  String name;

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

  abstract void prepare();
  // 피자를 만드는 데 필요한 재료들을 원재료 팩토리에서 가져옴.

  void bake() {
    System.out.println("175도에서 25분 간 굽기");
  }

  void cut() {
    System.out.println("피자를 사선으로 자르기");
  }

  void box() {
    System.out.println("상자에 피자를 담기");
  }

  void setName(String name) {
    this.name = name;
  }

  void getName() {
    return name;
  }

  public String toString() {
    // 피자 이름을 출력하는 부분
  }
}
profile
깃스타가 되고 싶은 벨플루언서

0개의 댓글