[Design Pattern] Command Pattern

younghyun·2022년 10월 26일
0

Design Pattern

목록 보기
11/14
post-thumbnail

Command Pattern 이란

실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있고, 기능 수정과 확장에 있어서 유연한 클래스를 설계하는 패턴이다.

설계

image
  • Command
    • 주문서 (클라이언트에게 받아서 웨이터에게 전달되는 종이)
    • 실행될 기능에 대한 인터페이스
    • 실행될 기능을 execute 메서드로 선언함
  • Invoker
    • 주문을 받아서 식사준비를 시키는 웨이터 (주문서 내용과 담당 셰프를 알 필요 없음)
    • 기능의 실행을 요청하는 호출자 클래스
    • Command 인터페이스만 알고 있고 실제로 어떻게 실행되는지 모름
  • Receiver
    • 명령을 받아 식사 준비하는 셰프
    • ConcreteCommand에서 execute 메서드를 구현할 때 필요한 클래스
    • ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스
  • ConcreteCommand
    • 실제로 실행되는 기능을 구현
    • 특정 행동과 Receiver 사이를 연결
  • Client
    • 무엇을 요청할지 결정하고, 요청 Command를 Invoker에 넘김
    • ConcreteCommand를 생성하고 Reciver를 설정
    • ex) main() 함수

예시

만능 리모컨

  • 기능
    • 차고문
    • 전등
    • TV
    • Stereo
    • 에어컨
  • 특징: 사용하려는 객체가 많고, API가 서로 다름

1. 커맨드 패턴을 활용

리모컨 버튼이 눌렸을 때 호출되는 코드와 실제로 일을 처리하는 코드를 분리시키는 것이 필요함
➜ 요청과 실행을 분리
➜ 어떤 것을 요구하는 객체 Invoker와 그 요구를 받아들이고 처리하는 객체 Receiver를 분리

image

Invoker의 요구 사항을 Receiver와 연결시킴으로써 일련의 행동을 캡슐화 한 것 Command
➜ 모든 일을 처리해주는 메서드를 만들어서 캡슐화 execute()

image

2. 클래스 구성

image

[1] Client에서 Receiver 생성
[2] Client에서 ConcreteCommand 생성
[3] 리모컨에 있는 버튼에 Command 지정
[4] 리모컨에 있는 버튼 클릭
[5] 해당 Command를 실행 (Command에 있는 내용은 Receiver에 있는 action을 실행시킴)

상세 클래스

  • 리모컨(버튼을 누르면 기능을 실행하는 기능)을 Invoker
  • 실제 디바이스(차고문, 전등, TV, Stereo, 에어컨)를 Receiver
  • 실제 디바이스를 wrap-up해서 상세 기능을 제공해주는 ConcreteCommand
  • 버튼에 실제 사용 객체를 연결해놓는 것을 Command
  • 리모컨을 사용하는 사용자 (버튼 기능을 인지하고 누름) 를 Client

흐름

  1. 리모컨의 각 슬롯에 명령을 할당한다.
  2. 리모컨이 Invoker가 되는 것이다.
  3. 사용자가 버튼을 누르면, 그 버튼에 연결된 커맨드 객체의 execute()메서드가 호출된다.
  4. Receiver(전등, 선풍기, 오디오 등)에서 특정 행동을 하는 메서드가 실행되어 작업을 처리한다.

3. 실제 구현

Command 인터페이스 만들기

  • 커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
public interface Command {
    public void execute();
    public void undo();  // 작업 취소 기능
}

전등을 켜기 위한 커맨드 클래스 구현 ConcreteCommand

// 커맨드 객체이므로 Command 인터페이스를 구현
public class LightOnCommand implements Command {
   
    // Receiver인 Light에 대한 reference
    Light light;   

    // 생성자에서 Receiver에 대한 reference, 이 커맨드 객체로 제어(on, off)할 전등 종류에 대한 정보 전달
    public LightOnCommand(Light light) {
        this.light = light;
    }

    // execute() 메서드가 호출될 때,
    // Recevier인 light 객체에 있는 on() 메서드가 호출된다.
    public void execute() {
        light.on();
    }
    
    public void undo() {
        light.off();
    }
}

전등을 끄기 위한 커맨드 클래스 구현 ConcreteCommand

public class LightOffCommand implements Command {
    Light light;

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

    public void execute() {
        light.off();
    }
    public void undo() {
        // 불이 꺼져있으면, 다시 켠다.
        light.on();
    }
}

버튼이 하나만 있는 리모컨 Invoker

public class simpleRemoteControl {

    // 커맨드를 넣을 하나의 슬롯으로 제어한다. 슬롯이 하나밖에 없음
    Command slot;

    public simpleRemoteControl() {}

    // 클라이언트에서 리모컨의 명령을 바꾸고 싶다면,
    // 커맨드 객체를 바꿔 끼울 수 있다.
    public void setCommand(Command command) {
        slot = command;
    }

    // 버튼이 눌려지면 이 메서드가 호출된다.
    // 지금 연결되어 있는 커맨드 객체의 execute() 메서드가 호출된다.
    public void buttonWasPressed() {
        slot.execute();
    }
}

리모컨 사용을 위한 테스트 클래스 Client

public class RemoteControlTest {
    public static void main(String[] args) {
    
        // remote 변수가 Invoker 역할
        // 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 받을 예정
        SimpleRemoteControl remote = new SimpleRemoteControl();
        
        // given: 요청을 받아서 처리할 Receiver인 Light 객체를 만든다.
        Light light = new Light();

        LightOnCommand lightOn = new LightOnCommand(light);

        // when: 커맨드 객체를 Invoker(remote 변수)에 전달
        remote.setCommand(lightOn);
        
        // then: Invoker인 remote에서 buttonWasPressed 메서드가 실행되면,
        // 지금 리모컨에 연결되어 있는 커맨드 객체(버튼에 연결된)인 lightOn 객체의 excute() 메서드를 실행
        remote.buttonWasPressed();
    }
}
// 실행 결과: Light is On

버튼이 on/off로 각각 7개씩, 총 14개의 버튼과 UNDO 버튼이 1개 있는 리모컨 Invoker

public class RemoteControlWithUndo {

    // 이 리모컨에서는 7개의 on/off 명령을 처리할 수 있으며
    // 여러 개의 Command를 호출해야 하므로 배열로 나타낸다.
    Command[] onCommands;
    Command[] offCommands;
    
    /*
     undoCommand가 배열이 아닌 이유?
     가장 최근 커맨드 객체 1개를 되돌리기 위해서 배열이 아닌 것이다.
     참조를 지속해서 바꿔준다.
     전체를 되돌리려면, Stack으로 구현한다.
     (Stack에서 pop()메서드로 빼내면 된다.)
    */
    
    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;
        }
        
        // 클라이언트가 다른 버튼을 누르지 않은 상태에서
        // UNDO 버튼을 누르더라도 문제가 생기지 않도록 만든다.
        undoCommand = noCommand;
    }
    
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        undoCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        
        // 클라이언트가 버튼을 누르면, 슬롯에 연결된 커맨드 객체의 execute()메서드를 호출한 후
        // 그 객체의 참조를 undoCommand에 바꿔 끼워준다.
        // ex) lighton을 호출했을 때 undo버튼을 눌렀을 때 lightoff기능을 실행시킬 수 있도록
        undoCommand = onCommands[slot];
    }

    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }
    
    // 클라이언트가 UNDO 버튼을 누르면,
    // undoCommand에 연결된 커맨드 객체의 undo() 메서드를 호출한다.
    public void undoButtonWasPushed() {
    
        // undo() 메서드가 실행되면,
        // 가장 최근에 했던 작업이 취소된다.
        undoCommand.undo();
    }

    public String toString() {
        // toString 코드...
    }
}

위의 리모컨 사용을 위한 테스트 클래스 Client

public class RemoteLoader {
    public static void main(String[] args) {
    
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
        
        Light livingRoomLight = new Light("Living Room");
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        System.out.println(remoteControl);
        
        remoteControl.undoButtonWasPushed();
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();
    }
}
profile
🌱 주니어 백엔드 개발자입니당

0개의 댓글