Web/JS

[JS] 디자인 패턴(Design Pattern)_커맨드 패턴(Command Pattern)

메바동 2023. 3. 2. 00:27
728x90

"JavaScript Design Pattern" 포스팅은 ChatGPT에게 "Essential design patterns for JavaScript developers to learn"로 질문한 뒤 나온 내용에 대해 정리하는 포스팅입니다.

 


 

1. 커맨드 패턴(Command Pattern)

커맨드 패턴은 요청이나 동작을 객체로 캡슐화하여 전달하고 조작하기 쉽게 만드는 행동 디자인 패턴이다.

JavaScript에서 커맨드 패턴은 동작 요청자와 동작을 수행하는 객체를 분리하는 데 사용되어 코드의 유연성과 재사용성을 향상한다.

 

JavaScript에서 커맨드 패턴은 다음 컴포넌트를 사용하여 구현할 수 있다.

 

  • Command: 모든 명령에 대한 계약을 정의하는 인터페이스이다. 일반적으로 단일 실행 메서드를 포함한다.
class Command {
  execute() {}
}

 

  • ConcreteCommand: 요청과 해당 매개 변수를 캡슐화하는 Command 인터페이스의 구현이다.
class ConcreteCommand extends Command {
  constructor(receiver, args) {
    super();
    this.receiver = receiver;
    this.args = args;
  }
  execute() {
    this.receiver.action(this.args);
  }
}

 

  • Receiver: 명령과 관련된 실제 작업을 수행하는 객체이다.
class Receiver {
  action(args) {
    console.log(`Performing action with ${args}`);
  }
}

 

  • Invoker: 명령을 보관하고 필요할 때 명령을 호출하는 객체이다.
class Invoker {
  constructor() {
    this.command = null;
  }
  setCommand(command) {
    this.command = command;
  }
  executeCommand() {
    this.command.execute();
  }
}

 

커맨드 패턴을 사용하면 Invoker의 인스턴스를 생성하고 그 위에 ConcreteCommand를 설정할 수 있다.

Invoker에서 executeCommand() 메서드를 실행하면 ConcreteCommand의 execute() 메서드가 호출되며, 이 메서드는 다시 Reciver의 action() 메서드를 호출한다.

 

const receiver = new Receiver();
const command = new ConcreteCommand(receiver, 'some arguments');
const invoker = new Invoker();
invoker.setCommand(command);
invoker.executeCommand(); // Logs "Performing action with some arguments"

 

이를 통해 요청을 보내는 객체와 요청과 관련된 작업을 수행하는 객체를 분리할 수 있으며, 보낸 사람과 수신자 객체를 분리할 수 있다.

또한, 커맨드 패턴은 요청과 해당 매개 변수를 별도의 객체에 캡슐화하기 때문에 기존 코드를 수정하지 않고도 새 명령 및 수신기를 쉽게 추가할 수 있다.

 

다음은 JavaScript에서 커맨드 패턴을 구현하는 예이다:

 

// Receiver 객체
class Light {
  turnOn() {
    console.log('Light is on');
  }

  turnOff() {
    console.log('Light is off');
  }
}

// Command 객체
class LightOnCommand {
  constructor(light) {
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }
}

// Command 객체
class LightOffCommand {
  constructor(light) {
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }
}

// Invoker 객체
class RemoteControl {
  constructor() {
    this.commands = [];
  }

  addCommand(command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach((command) => command.execute());
    this.commands = [];
  }
}

// Usage
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);

const remoteControl = new RemoteControl();
remoteControl.addCommand(lightOnCommand);
remoteControl.addCommand(lightOffCommand);
remoteControl.executeCommands();

 

이 예제에는 각각 조명을 켜고 끄는 메서드인 turnOn()과 turnOff()가 있는 Light 객체가 있다.

또한, Light 객체를 생성자 인수로 사용하고 Light 객체에서 해당 메서드를 호출하는 execute() 메서드를 구현하는 두 개의 Command 객체 LightOnCommand와 LightOffCommand가 있다. 

그리고 Command 객체 목록을 유지하고 execute() 메서드를 호출하여 실행할 수 있는 Invoker 객체 RemoteControl이 있다.

RemoteControl의 commands 목록에 LightOnCommand와 LightOffCommand 객체를 추가한 다음, remoteControl.executeCommands()를 호출하여 한 번에 모두 실행할 수 있다.

 

이러한 방식으로 커맨드 패턴을 사용하면 작업을 요청하는 코드(Invoker)와 작업을 수행하는 코드(Receiver 및 Command 객체)를 분리할 수 있다.

따라서, Invoker 또는 Receiver 코드를 수정하지 않고도 새 명령을 쉽게 추가할 수 있으며, 다른 컨텍스트에서 명령을 재사용하는 것도 쉽다.

 

 

 

2. Command Pattern 연습 코드

ChatGPT에게 "Give me a challenge to practice the Command Pattern in JavaScript."라고 질문한 뒤 나온 문제와 그에 대한 답이다.

 

 

ChatGPT가 계속 스켈레톤 코드가 아닌 완성된 코드를 제시하여 커맨드 패턴의 연습 코드는 ChatGPT가 작성한 코드만 올리기로 한다.

 

// Receiver object
class TV {
  constructor() {
    this.isOn = false;
    this.channel = 1;
    this.volume = 50;
  }

  turnOn() {
    this.isOn = true;
    console.log('TV가 켜졌습니다.');
  }

  turnOff() {
    this.isOn = false;
    console.log('TV가 꺼졌습니다.');
  }

  changeChannelUp() {
    if (this.isOn) {
      this.channel++;
      console.log(`채널이 ${this.channel}(으)로 변경되었습니다.`);
    }
  }

  changeChannelDown() {
    if (this.isOn) {
      this.channel--;
      console.log(`채널이 ${this.channel}(으)로 변경되었습니다.`);
    }
  }

  volumeUp() {
    if (this.isOn) {
      this.volume++;
      console.log(`볼륨이 ${this.volume}(으)로 조정되었습니다.`);
    }
  }

  volumeDown() {
    if (this.isOn) {
      this.volume--;
      console.log(`볼륨이 ${this.volume}(으)로 조정되었습니다.`);
    }
  }
}

// Command object
class TVOnCommand {
  constructor(tv) {
    this.tv = tv;
  }

  execute() {
    this.tv.turnOn();
  }

  undo() {
    this.tv.turnOff();
  }
}

// Command object
class TVOffCommand {
  constructor(tv) {
    this.tv = tv;
  }

  execute() {
    this.tv.turnOff();
  }

  undo() {
    this.tv.turnOn();
  }
}

// Command object
class TVChangeChannelUpCommand {
  constructor(tv) {
    this.tv = tv;
    this.previousChannel = null;
  }

  execute() {
    this.previousChannel = this.tv.channel;
    this.tv.changeChannelUp();
  }

  undo() {
    this.tv.channel = this.previousChannel;
    console.log(`채널이 ${this.previousChannel}(으)로 변경되었습니다.`);
  }
}

// Command object
class TVChangeChannelDownCommand {
  constructor(tv) {
    this.tv = tv;
    this.previousChannel = null;
  }

  execute() {
    this.previousChannel = this.tv.channel;
    this.tv.changeChannelDown();
  }

  undo() {
    this.tv.channel = this.previousChannel;
    console.log(`채널이 ${this.previousChannel}(으)로 변경되었습니다.`);
  }
}

// Command object
class TVVolumeUpCommand {
  constructor(tv) {
    this.tv = tv;
    this.previousVolume = null;
  }

  execute() {
    this.previousVolume = this.tv.volume;
    this.tv.volumeUp();
  }

  undo() {
    this.tv.volume = this.previousVolume;
    console.log(`볼륨이 ${this.previousVolume}(으)로 조정되었습니다.`);
  }
}

// Command object
class TVVolumeDownCommand {
  constructor(tv) {
    this.tv = tv;
    this.previousVolume = null;
  }

  execute() {
    this.previousVolume = this.tv.volume;
    this.tv.volumeDown();
  }

  undo() {
    this.tv.volume = this.previousVolume;
    console.log(`볼륨이 ${this.previousVolume}(으)로 조정되었습니다.`);
  }
}

// Invoker object
class RemoteControl {
  constructor() {
    this.commands = [];
    this.undoStack = [];
  }

  setCommand(command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach((command) => {
      command.execute();
      this.undoStack.push(command);
    });
    this.commands = [];
  }

  undoLastCommand() {
    const commandToUndo = this.undoStack.pop();
    if (commandToUndo) {
      commandToUndo.undo();
    }
  }
}

// Usage
const tv = new TV();
const remote = new RemoteControl();

// Create command objects
const turnOnCommand = new TVOnCommand(tv);
const turnOffCommand = new TVOffCommand(tv);
const changeChannelUpCommand = new TVChangeChannelUpCommand(tv);
const changeChannelDownCommand = new TVChangeChannelDownCommand(tv);
const volumeUpCommand = new TVVolumeUpCommand(tv);
const volumeDownCommand = new TVVolumeDownCommand(tv);

// Set commands to the remote control
remote.setCommand(turnOnCommand);
remote.setCommand(changeChannelUpCommand);
remote.setCommand(volumeUpCommand);
remote.setCommand(volumeUpCommand);
remote.setCommand(volumeUpCommand);

// Execute commands and undo the last command
remote.executeCommands();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand();
console.log('현재 TV 상태:', tv);

remote.undoLastCommand(); // 실행되지 않음
console.log('현재 TV 상태:', tv);
728x90