팩토리 메소드 패턴은 객체의 새성 코드를 별도의 클래스 또는 메소드로 분리함으로써 객체 생성의 변화에 대비하는데 유용하다.
프로그램이 제공하는 기능은 상황에 따라 변경될 수 있다. 그리고 특정 기능의 구현은 개별 클래스를 통해 제공되는 것이 바람직한 설계다.
그러므로 기능의 변경이나 상황에 따른 기능의 선택은 바로 해당 객체를 생성하는 코드의 변경을 초래한다. 게다가 상황에 따라 적절한 코드를 생성하는 코드는 자주 중복 될 수 있다. 이런 경우 객체 생성 방식의 변화는 해당되는 모든 코드 부분을 변경해야 하는 문제를 야기한다.
팩토리 메소드 패턴의 개념
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 객체를 복수 개 갖는다.
ElevatorManager의 requestElevator()는 요청받았을 때 우선 ThroughputScheduler클래스의 selectElevator()를 호출해 적절한 엘리베이터를 선택한다.
그리고 선택된 엘리베이터에 해당하는 ElevatorController인스턴스의 goToFloor()를 호출해 엘리베이터를 이동시킨다.
문제점
이 코드는 다음과 같은 문제가 있다.
현재 ElevatorManager클래스는 ThroughputScheduler 클래스를 이용한다. 즉, 엘리베이터의 처리량을 최대화시키는 전략을 사용한다는 의미다. 만약 다른 스케줄링 전략을 사용해야한다면?
프로그램 실행 중 스케줄링 전략을 변경, 즉 동적 스케줄링을 지원해야한다면?
우선 대기시간을 최소화 하는 전략이 추가된다고 하자. 해당 스케줄링을 ResponseTimeScheduler라고 한다.
기존에는 ElevatorManager에서 ThroughputScheduler객체를 이용했지만 이제는 실행중에 스케줄러 전략이 변경될 수 있어야 한다.
처리량 전략이나 대기시간 전략 모두 ElevatorManager입장에서는 엘리베이터 스케줄링 전략의 일종이다. 그러므로 ElevatorManager는 두 객체를 생성한 후 이를 각ㄱ의 클래스가 안이라 ElevatorScheduler 인터페이스를 통해서 사용한다. 이는 결과적으로 Strategy패턴을 활용한다.
이제 두 스케쥴링 전략을 모두 사용할 수 있지만 여전히 문제는 있다. 다른 전략이 필요할때마다 ElevatorManager의 requestElevator()가 여전히 변경되야 하기 때문이다.
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