본문 바로가기

ComputerScience/DesignPattern

[DesignPattern] Command Pattern

커맨드 패턴(Command Pattern)

커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있습니다.

이미지 출처: https://en.wikipedia.org/wiki/Command_pattern

 

위의 UML Class Diagram 에서 Invoker 클래스는 직접 요청을 구현하지 않습니다.

대신 Invoker는 요청을 수행하기 위해 Command 인터페이스를 참조합니다(command.execute()).

이로써 Invoker는 요청이 어떻게 수행되는지와는 독립적이게 됩니다.

Command1 클래스는 Command 인터페이스를 구현하여 Receiver1 객체에 대한 작업을 수행합니다 (receiver1.action1()).

UML Sequence Diagram은 런타임 상호 작용을 보여줍니다.

Invoker 객체가 Command1 객체에서 execute()를 호출합니다. Command1은 Receiver1 객체에서 action1()을 호출하고, 이로써 요청이 수행됩니다.

 

예시를 보겠습니다.

Command

Command 객체는 일련의 행동을 특정 Receiver와 연결함으로써 해당 Receiver와 그에 대한 요청을 캡슐화합니다..

이를 위해서는 행동과 Receiver를 한 객체에 넣고, execute()라는 메서드 하나만 외부에 공개해야 합니다.

모든 명령은 execute() 메서드 호출로 수행되며, 이 메소드는 Receiver에 특정 작업을 처리하라는 지시를 전달합니다.

public interface Command {
    public void execute();
}

 

Receiver 

Receiver는 요구 사항을 수행할 때 어떤 일을 처리해야 하는지를 알고 있는 객체입니다.

아래는 Receiver 역할을 하는 Light 객체입니다. 

public class Light {

    public void on() {
        System.out.println("Light is on");
    }

    public void off() {
        System.out.println("Light is off");
    }

}

 

Concrete Command

Command 인터페이스의 구현체로, 특정 행동과 Receiver를 연결해줍니다.
Invoker에서 execute() 호출로 요청을 하면, 이 객체에서 Receiver에 있는 메서드를 호출해서 요청된 작업을 처리합니다.

 

이 예제에선 Invoker가 Command 를 구현한 LightOnCommand의 execute() 호출을 하면 Receiver 객체인 Light의 행동을 호출합니다.

public class LightOnCommand implements Command {

    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

}

 

Invoker

Invoker 클래스에는 Command를 가지고 있습니다.
execute() 메소드를 호출함으로써 Command 객체에 특정 작업을 수행해달라는 요구를 합니다.

 

아래 SimpleRemoteControl은 인자로 Command를 받고, 그 버튼이 클릭되었을 때 Command의 execute 메소드를 호출합니다.

 public class SimpleRemoteControl {
	Command slot;
    
    public SimpleRemoteControl() {}
    
	public void setCommand(Command command) {
 		slot = command;
	}
    
	public void buttonWasPressed() {
		slot.execute();
	}
}

main

public class RemoteControlTest {

    public static void main(String[] args) {
        // Invoker
        SimpleRemoteControl remote = new SimpleRemoteControl();

        // Receiver
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();

        // Command
        LightOnCommand lightOn = new LightOnCommand(light);
        GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor);

        // light
        remote.setCommand(lightOn); 
        remote.buttonWasPressed();
        
        // garage
        remote.setCommand(garageOpen); 
        remote.buttonWasPressed();
    }

}

 

Output

Light is on
Garage Door is Open

 

기능 추가: 이전 작업 취소하기

 

속도의 상태가 4가지(HIGH, MEDIUM, LOW, OFF) 중 하나인 선풍기가 있다고 칩시다.
우리는 작업 취소 기능을 호출하면 이전 속도로 되돌리는 기능을 구현하고 싶습니다.

 

선풍기 코드

public class CeilingFan {

    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;

    String location;
    int speed;
    
    public CeilingFan(String location) {
        this.location = location;
        speed = OFF;
    }

    public int getSpeed() {
        return speed;
    }

    public void high() {
        // turns the ceiling fan on to high
        speed = HIGH;
        System.out.println(location + " ceiling fan is on high");
    }

    public void medium() {
        // turns the ceiling fan on to medium
        speed = MEDIUM;
        System.out.println(location + " ceiling fan is on medium");
    }

    public void low() {
        // turns the ceiling fan on to low
        speed = LOW;
        System.out.println(location + " ceiling fan is on low");
    }

    public void off() {
        // turns the ceiling fan off
        speed = OFF;
        System.out.println(location + " ceiling fan is off");
    }

}

 

1. 인터페이스 Command에 undo() 메서드 추가

public interface Command {

    public void execute();

    public void undo();

}

 

2. Command 객체에 undo()를 구현, 선풍기의 이전 속도 상태를 저장하는 필드를 추가

public class CeilingFanHighCommand implements Command {

    CeilingFan ceilingFan;
    int prevSpeed; 

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }

    @Override
    public void execute() {
        prevSpeed = ceilingFan.getSpeed(); // save previous speed
        ceilingFan.high();
    }

    @Override
    public void undo() {
    
        switch (prevSpeed) {
            case CeilingFan.HIGH:
                ceilingFan.high();
                break;

            case CeilingFan.MEDIUM:
                ceilingFan.medium();
                break;

            case CeilingFan.LOW:
                ceilingFan.low();
                break;

            case CeilingFan.OFF:
                ceilingFan.off();
                break;
        }
    }

}

 

3. Invoker에 undoCommand 필드 추가, Comman 객체의 undo()를 호출하는 메서드를 추가

public class RemoteControlWithUndo {

    Command[] onCommands;
    Command[] offCommands;
    
    Command undoCommand;

    public RemoteControlWithUndo() {
        onCommands = new Command[7];
        offCommands = new Command[7];

        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    ...

    public void undoButtonWasPushed() {
        undoCommand.undo(); // call undo()
    }

    public String toString() {
        ...
        stringBuilder.append("[undo] " + undoCommand.getClass().getName() + "\n");
        return stringBuilder.toString();
    }

}

 

main

public class RemoteLoader {

    public static void main(String[] args) {
        // Invoker
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();

        // Receiver
        CeilingFan ceilingFan = new CeilingFan("Living Room");

        // Command
        CeilingFanMediumCommand ceilingFanMedium = new CeilingFanMediumCommand(ceilingFan);
        CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        // Set Command
        remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
        remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);

        // Test
        remoteControl.onButtonWasPushed(0); 
        remoteControl.offButtonWasPushed(0); 
        System.out.println(remoteControl);

        remoteControl.undoButtonWasPushed(); // undo

        remoteControl.onButtonWasPushed(1); 
        System.out.println(remoteControl);

        remoteControl.undoButtonWasPushed(); // undo
    }

}

 

커맨드 패턴의 사용 

GUI 버튼 및 메뉴 항목

- Swing 및 Borland Delphi 프로그래밍에서는 Command 객체를 사용하여 GUI 버튼이나 메뉴 항목을 초기화할 수 있습니다. Command 객체는 원하는 명령을 수행할 뿐만 아니라 아이콘, 키보드 단축키, 툴팁 텍스트 등과 관련된 정보를 가질 수 있습니다.

 

다중 실행 취소

- 프로그램의 모든 사용자 동작이 명령 객체로 구현된 경우, 프로그램은 최근에 실행된 명령 객체의 스택을 유지할 수 있습니다. 사용자가 명령을 실행 취소하려면 프로그램은 가장 최근에 실행된 명령 객체를 팝하고 해당 undo() 메서드를 실행합니다.

 


네트워킹

   - Command 객체를 통째로 네트워크를 통해 다른 기계에서 실행할 수 있습니다. 예를 들어 컴퓨터 게임에서 플레이어 동작을 전송할 수 있습니다.

 

병렬 처리

   - 명령이 공유 리소스에 대한 작업으로 작성되고 여러 스레드에서 병렬로 실행될 때 사용됩니다. 종종 이것은 master/worker 패턴으로 언급됩니다.

 

Thread pool

   - 명령 객체를 사용하여 일반적인 목적의 스레드 풀 클래스를 구현할 수 있습니다. 스레드 풀은 작업을 대기 중인 작업 큐에 추가하는 public addTask() 메서드를 가지고 있습니다. 큐의 항목은 명령 객체이며, 일반적으로 이 객체는 java.lang.Runnable과 같은 공통 인터페이스를 구현하여 스레드 풀이 특정 작업에 대한 지식 없이도 명령을 실행할 수 있게 합니다.


Transaction

   - 실행된 또는 실행될 작업 목록을 유지함으로써 데이터베이스 엔진이나 소프트웨어 설치 프로그램과 같은 프로그램은 트랜잭션 동작을 구현할 수 있습니다. 한 작업이 실패하면 다른 모든 작업을 되돌리거나 폐기할 수 있습니다(roll back).

 

참고 자료

https://github.com/IT-Book-Organization/HeadFirst-DesignPattern/blob/main/Chapter_06/README.md

https://en.wikipedia.org/wiki/Command_pattern

 

Command pattern - Wikipedia

From Wikipedia, the free encyclopedia Behavioral design pattern In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at

en.wikipedia.org