ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Design Pattern] Factory Method & Abstract Factory Patterns
    Learn/Architecture 2022. 8. 29. 23:35

    # 개요

    아래와 같이 conditional statement에 따라 다른 Concrete Class를 불러야 하는 상황을 피할 때 이 패턴을 쓴다. 

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

     

    새로운 object를 new를 쓰지 않고 만드는 것을 Factory Pattern이라고 한다. 

    • Factory Method: 상속(inheritance)를 통해서 인스턴스를 생성하는 방식
    • Abstract Factory: 위임(Delegation)을 통해서 인스턴스를 생성하는 방식

     

    SOLID 원칙의 Dependency Inversion Priciple(DIP)와 관련있다. 

     

    # Simple Factory

    아래의 코드에서는 피자의 종류가 바뀌면 해당하는 조건문과 클래스 찾아서를 바꿔줘야 한다. 

    Pizza orderPizza(String type) {
        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;
    }

    하지만 아래와 같이 생성만 전문으로 하는 factory를 만들어주면 코드가 훨씬 깔끔해진다.  

    public class SimplePizzaFactory {
        public Pizza createPizza(String type) {
            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();
            return pizza;
        }
    }

    다이어그램을 그리면 다음과 같다. 

    이렇게 그리면 PizzaStore가 Concrete Product의 변경/추가에 영향을 받지 않아서 좋다. 

    # Factory Method Pattern

    오브젝트를 만드는 메서드를 노출시키고, 서브클래스가 그 일을 하도록 하는 방식 (Inheritance)

     

    예시

    피자가게의 지점에 따라 피자 스타일이 다를 것이므로 아래와 같이 코드를 작성할 수 있다. 

    NYPizzaFactory nyFactory = new NYPizzaFactory();
    PizzaStore nyStore = new PizzaStore(nyFactory);
    nyStore.order(“Veggie”);
    
    ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
    PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
    chicagoStore.order(“Veggie”);

    order에는 피자를 만드는 것 외에 공통적인 작업들이 포함되어 있다. 

    이 작업을 Factory Method Pattern으로 작성하면 다음과 같다. (주석된 부분이 중요)

    public abstract class PizzaStore {
        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);
    }

    그러면 하위 클래스에서는 각각 createPizza부분만 구현해주면 order의 나머지 부분은 공통적으로 사용할 수 있다. 

     

    일반화하면 다음과 같다. 

    • 오브젝트를 생성하는 인터페이스를 정의하고, 그 인터페이스는 서브클래스에서 구현
    • Creator는 공통적인 부분(anOperation)을 제공
    • 어떤 Product가 필요한지는 서브클래스에서 구현 (클라이언트만 아는 부분)

     

    # Abstract Factory Pattern

    • 서브클래스가 아닌, creation을 전문으로 하는 인터페이스를 만들어서 위임(delegation)하는 방식
    • Factory Method Inheritance를 사용하는 반면, Abstract Factory는 composition&delegation을 사용한다. 
    • Factory Method는 클래스 범위인 반면, Abstract Factory는 오브젝트 범위이다. 

     

    예시

    피자 재료에 대한 Factory가 다음과 같다고 하자. 

    public interface PizzaIngredientFactory {
        public Dough createDough();
        public Sauce createSauce();
        public Cheese createCheese();
        ...
    }

    위 인터페이스를 가지고 뉴욕 스타일의 팩토리도 구현할 수 있다. 

    public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
        public Dough createDough() {
            return new ThinCrustDough();
        }
    
        public Sauce createSauce() {
            return new MarinaraSauce();
        }
        ...
    }

    뉴욕피자를 만들고자 할때는 뉴욕스타일의 팩토리로 인스턴스를 생성해주면 된다. 

     

    그리고 피자를 만드는 부분에서는 ingredientFactory에 어떤 피자를 만들지를 위임해주면 된다. 

    Abstract Factory에서는 직접 concrete object를 넘겨주는게 아니라 일반적인 형태로 작성한다. 

    public class NYPizzaStore extends PizzaStore {
        protected Pizza createPizza(String item) {
            Pizza pizza = null;
            // 뉴욕 피자에 대한 Factory 생성
            PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
            
            if (item.equals("cheese")) {
                /* 
                뉴욕스타일을 만들어라고 명시적으로 직접 넘겨주는게 아니라
                ingredientFactory에 위임하여 선택하도록 함
                */
                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;
        }
    }

    치즈 피자를 만드는 코드에는 어떤 스타일에 대한 것인지가 표현될 필요가 없다. (장점)

    • 클라이언트 코드가 변경에 영향을 받지 않는다. 

    아래와 같이 ingredientFactory가 결정해서 넘겨주는대로 하면 된다. 

    public class CheesePizza extends Pizza {
        PizzaIngredientFactory ingredientFactory;
        
        public CheesePizza(PizzaIngredientFactory ingredientFactory) {
            this.ingredientFactory = ingredientFactory;
        }
        
        void prepare() {
            System.out.println("Preparing " + name);
            dough = ingredientFactory.createDough();
            sauce = ingredientFactory.createSauce();
            cheese = ingredientFactory.createCheese();
        }
    }

     

    장점

    • concrete class로부터 클라이언트가 자유로워진다. 
      • 아래 그림처럼 ConcreteFactory3이 들어와도 Client는 영향이 없다. 

    • 사용할 product family를 바꾸는 것이 쉽다. (인스턴스를 갈아끼면 된다)

    단점

    • 생각못했던 새로운 종류의 product가 나오면 인터페이스가 바뀌므로 변경이 어렵다. 

     

    # QUIZ

    아래의 코드에 Factory Method Pattern을 적용하여 개선하시오. 

    (힌트: Database는 product)

    public interface Database {}
    public class AlphaDatabase implements Database {}
    public class DeltaDatabase implements Database {}
    
    public class BadClient {
        public Map queryDatabase(String queryString) {
            Database db = new AlphaDatabase();
            checkQueryIsValid(queryString);
            return db.find(queryString);
        }
    }

     

    정답

    이 코드의 문제점은 AlphaDatabse 인스턴스가 직접 사용되고 있다는 것이다. 

    예를들어 DeltaDatabase도 있다면 해당 코드를 복사해서 저 부분만 바꿔줘야한다. 

     

    아래와 같이 패턴을 적용하면 새로운 데이터베이스가 들어와도 클라이언트는 자유롭다. 

    public abstract class TemplateClient {
        public Map queryDatabase(String queryString) {
            // createDatabase로 변경
            Database db = createDatabase ();
            checkQueryIsValid(queryString);
            return db.find(queryString);
        }
        // 여기는 서브클래스에서 구현한다. 
        protected abstract Database createDatabase ();
    }
    
    public class AlphaClient extends TemplateClient {
        public Database createDatabase () {
            return new AlphaDatabase();
        }
    }
    
    public class DeltaClient extends TemplateClient {
        public Database createDatabase () {
            return new DeltaDatabase();
        }
    }

     

    'Learn > Architecture' 카테고리의 다른 글

    [Design Pattern] Singleton Pattern  (0) 2022.08.31
    [Design Pattern] Builder Pattern  (0) 2022.08.30
    [Design Pattern] Mediator Pattern  (0) 2022.08.29
    [Design Pattern] State Pattern  (0) 2022.08.29
    [Design Pattern] Iteration Pattern  (0) 2022.08.28

    댓글

Designed by Tistory.