Factory method 패턴

  • 팩토리 메소드 패턴은 객체의 새성 코드를 별도의 클래스 또는 메소드로 분리함으로써 객체 생성의 변화에 대비하는데 유용하다.
  • 프로그램이 제공하는 기능은 상황에 따라 변경될 수 있다. 그리고 특정 기능의 구현은 개별 클래스를 통해 제공되는 것이 바람직한 설계다.
  • 그러므로 기능의 변경이나 상황에 따른 기능의 선택은 바로 해당 객체를 생성하는 코드의 변경을 초래한다. 게다가 상황에 따라 적절한 코드를 생성하는 코드는 자주 중복 될 수 있다. 이런 경우 객체 생성 방식의 변화는 해당되는 모든 코드 부분을 변경해야 하는 문제를 야기한다.

팩토리 메소드 패턴의 개념

class A {
    void f(){
        X x;
        if(...)
            x = new X1();
        else
            x = new X2();
        x.f1();
    }
}
class Z {
    void f(){
        X x;
        if(...)
            x = new X1();
        else
            x = new X2();
        x.f2();
    }
}
  • 여기서 A클래스와 Z클래스는 필요에 따라서 X1과 X2의 인스턴스를 생성하고 있다. 만약 X1과 X2의 생성 방식이 달라지거나 X3와 같이 새로운 클래스를 생성해야 한다면 X1,X2를 생성하는 모든 코드를 수정해야 한다.
  • 아래와 같이수정해보자.
class Factory {
    static X getX(...){
        X x;
        if(...)
            x = new X1();
        else
            x= new X2();
        return x;
    }
}
class A{
    void f(){
        X x = Factory.getX(...);
        x.f1();
    }
}

class B{
    void f(){
        X x = Factory.getX(...);
        x.f2()
    }
}
  • 팩토리 메서드 패턴을 사용하면 인스턴스 생성 기능을 제공하는 Factory클래스를 정의하고 이를 활용하는 방식으로 설계하면 된다.
  • X3를 추가해야할때 Factory만 수정하면 되고 A클래스와 Z클래스는 변경할 필요가 없다.

여러가지 방식의 엘리베이터 스케줄링 제공하기

  • 엘리베이터가 1대만 있는 경우가 아니라 여러대의 엘리베이터가 있는 경우, 만약 엘리베이터 내부에서 버튼을 눌렀을때는 해당 사용자가 탄 엘리베이터를 이동시키면 된다.
  • 그런데 사용자가 건물의 호출 버튼을 누르는 경우에는 여러대의 엘리베이터중 하나를 선택해 이동해야 한다.
  • 이와 같이 주어진 요청을 받았을때 여러 개의 인스턴스 중 하나를 선택하는 것을 'Scheduling'이라고 한다.
public class ElevatorManager {

    private List<ElevatorController> controllers;
    private ThroughputScheduler scheduler;

    public ElevatorManager(int controllerCount) {
        controllers = new ArrayList<ElevatorController>(controllerCount);
        for (int i = 0; i < controllerCount; i++) {
            ElevatorController controller = new ElevatorController(i);
            controllers.add(controller);
        }
        scheduler = new ThroughputScheduler();
    }

    void requestElevator(int destination, Direction direction) {
        int selecedElevator = scheduler.selectElevator(this, destination, direction);

        controllers.get(selecedElevator).goToFloor(destination);
    }

}

public class ElevatorController {

    private int id;
    private int currentFloor;

    public ElevatorController(int id) {
        this.id = id;
        currentFloor = 1;
    }

    public void goToFloor(int destination) {
        System.out.printf("Elevator [" + id + "] Floor: " + currentFloor);
        currentFloor = destination;
        System.out.println(" ==> " + currentFloor);
    }
}
public class ThroughputScheduler {

    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }
  • ElevatorManager는 이동요청을 처리하는 클래스로 엘리베이터를 스케줄링하기 위한 ThroughputScheduler객체를 갖는다. 그리고 각 엘리베이터의 이동을 책임지는 ElevatorController 객체를 복수 개 갖는다.
  • ElevatorManagerrequestElevator()는 요청받았을 때 우선 ThroughputScheduler클래스의 selectElevator()를 호출해 적절한 엘리베이터를 선택한다.
  • 그리고 선택된 엘리베이터에 해당하는 ElevatorController인스턴스의 goToFloor()를 호출해 엘리베이터를 이동시킨다.

문제점

  • 이 코드는 다음과 같은 문제가 있다.
    • 현재 ElevatorManager클래스는 ThroughputScheduler 클래스를 이용한다. 즉, 엘리베이터의 처리량을 최대화시키는 전략을 사용한다는 의미다. 만약 다른 스케줄링 전략을 사용해야한다면?
    • 프로그램 실행 중 스케줄링 전략을 변경, 즉 동적 스케줄링을 지원해야한다면?
  • 우선 대기시간을 최소화 하는 전략이 추가된다고 하자. 해당 스케줄링을 ResponseTimeScheduler라고 한다.
  • 기존에는 ElevatorManager에서 ThroughputScheduler객체를 이용했지만 이제는 실행중에 스케줄러 전략이 변경될 수 있어야 한다.
  • 처리량 전략이나 대기시간 전략 모두 ElevatorManager입장에서는 엘리베이터 스케줄링 전략의 일종이다. 그러므로 ElevatorManager는 두 객체를 생성한 후 이를 각ㄱ의 클래스가 안이라 ElevatorScheduler 인터페이스를 통해서 사용한다. 이는 결과적으로 Strategy패턴을 활용한다.
  • 이제 두 스케쥴링 전략을 모두 사용할 수 있지만 여전히 문제는 있다. 다른 전략이 필요할때마다 ElevatorManagerrequestElevator()가 여전히 변경되야 하기 때문이다.
  • requestElevator()가 하는 일은 엘리베이터 선택과 엘리베이터의 이동이다. 그러므로 requestElevator()가 변경되는것은 바람직하지 않다.

해결책

  • 이러한 문제를 해결하려면 주어진 기능을 실제로 제공하는 적절한 클래스 생성 작업을 별도의 클래스/메소드로 분리시키는 편이 좋다.
  • 스케쥴링 전략에 일치하는 클래스를 생성하는 코드를 requestElevator()에서 분리해서 별도의 클래스/메소드를 정의하면 된다.
  • 다음은 스케줄링 전략에 맞는 객체를 생성하는 기능을 구현한 코드다
public enum SchedulingStrategyID {RESPONSE_TIME, THROUGHPUT, DYNAMIC}

public class SchedulerFactory {

    public static ElevatorScheduler getScheduler(SchedulingStrategyID strategyID) {
        ElevatorScheduler scheduler = null;
        switch (strategyID) {
            case RESPONSE_TIME:
                scheduler = new ResponseTimeScheduler();
                break;
            case THROUGHPUT:
                scheduler = new ThroughputScheduler();
                break;
            case DYNAMIC:
                int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

                if (hour < 12) {
                    scheduler = new ResponseTimeScheduler();
                } else {
                    scheduler = new ThroughputScheduler();
                }
        }
        return scheduler;
    }
}

public class ElevatorManager {

    private List<ElevatorController> controllers;
    private SchedulingStrategyID strategyID;

    public ElevatorManager(int controllerCount, SchedulingStrategyID strategyID) {
        controllers = new ArrayList<>(controllerCount);

        for (int i = 0; i < controllerCount; i++) {
            ElevatorController controller = new ElevatorController(i + 1);
            controllers.add(controller);
        }
        this.strategyID = strategyID;
    }

    void requestElevator(int destination, Direction direction) {
        ElevatorScheduler scheduler = SchedulerFactory.getScheduler(strategyID);
        System.out.println(scheduler);
        int selectedElevator = scheduler.selectElevator(this, destination, direction);
        controllers.get(selectedElevator).goToFloor(destination);
    }

}

public class Client {

    public static void main(String[] args) {
        ElevatorManager emWResponseTimeScheduler =
            new ElevatorManager(2, SchedulingStrategyID.RESPONSE_TIME);
        emWResponseTimeScheduler.requestElevator(10, Direction.UP);
        emWResponseTimeScheduler.requestElevator(3, Direction.UP);
        emWResponseTimeScheduler.requestElevator(1, Direction.UP);
        emWResponseTimeScheduler.requestElevator(5, Direction.DOWN);

        ElevatorManager emWTPScheduler =
            new ElevatorManager(2, SchedulingStrategyID.THROUGHPUT);
        emWTPScheduler.requestElevator(10, Direction.UP);

        ElevatorManager emWDynamicScheduler =
            new ElevatorManager(2, SchedulingStrategyID.DYNAMIC);
        emWDynamicScheduler.requestElevator(10, Direction.UP);
    }

}
  • 아래는 실행결과다
ResponseTimeScheduler@3cb5cdba
Elevator [1] Floor: 1 ==> 10
ResponseTimeScheduler@56f4468b
Elevator [1] Floor: 10 ==> 3
ResponseTimeScheduler@6cc4c815
Elevator [1] Floor: 3 ==> 1
ResponseTimeScheduler@3a82f6ef
Elevator [1] Floor: 1 ==> 5
ThroughputScheduler@643b1d11
Elevator [1] Floor: 1 ==> 10
ThroughputScheduler@457e2f02
Elevator [1] Floor: 1 ==> 10
  • 나머지 코드
  • 이 경우도 스케줄러가 여러개 생성되는 것을 알 수 있다. 이것을 방지하기 위해 싱글톤 패턴을 적용해주면 된다.
public class SchedulerFactory {

    public static ElevatorScheduler getScheduler(SchedulingStrategyID strategyID) {
        ElevatorScheduler scheduler = null;
        switch (strategyID) {
            case RESPONSE_TIME:
                scheduler = ResponseTimeScheduler.getInstance();
                break;
            case THROUGHPUT:
                scheduler = ThroughputScheduler.getInstance();
                break;
            case DYNAMIC:
                int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

                if (hour < 12) {
                    scheduler = ResponseTimeScheduler.getInstance();
                } else {
                    scheduler = ThroughputScheduler.getInstance();
                }
        }
        return scheduler;
    }
}

public class ResponseTimeScheduler implements ElevatorScheduler {

    private static ElevatorScheduler scheduler;

    private ResponseTimeScheduler() {

    }

    public static ElevatorScheduler getInstance() {
        if (scheduler == null) {
            scheduler = new ResponseTimeScheduler();
        }
        return scheduler;
    }

    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 1;
    }

}

public class ThroughputScheduler implements ElevatorScheduler {

    private static ElevatorScheduler scheduler;

    private ThroughputScheduler() {

    }

    public static ElevatorScheduler getInstance() {
        if (scheduler == null) {
            scheduler = new ThroughputScheduler();
        }
        return scheduler;
    }

    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }

}
  • 나머지 코드는 동일하다
  • 아래는 싱글톤 패턴으로 바꾸고 난 뒤의 실행결과다
ResponseTimeScheduler@3cb5cdba
Elevator [2] Floor: 1 ==> 10
ResponseTimeScheduler@3cb5cdba
Elevator [2] Floor: 10 ==> 3
ResponseTimeScheduler@3cb5cdba
Elevator [2] Floor: 3 ==> 1
ResponseTimeScheduler@3cb5cdba
Elevator [2] Floor: 1 ==> 5
ThroughputScheduler@6cc4c815
Elevator [1] Floor: 1 ==> 10
ThroughputScheduler@6cc4c815
Elevator [1] Floor: 1 ==> 10
  • 이제 스케줄러 인스턴스가 하나만 생성된것을 알 수 있다.

Comments