JUINTINATION

커맨드(Command) 패턴 본문

JAVA객체지향디자인패턴

커맨드(Command) 패턴

DEOKJAE KWON 2023. 7. 6. 13:52
반응형

커맨드 패턴이란?

실행될 기능을 캡슐화하여 기능의 실행을 요구하는 호출자 클래스(Invoker)와 실제 기능을 실행하는 수신자 클래스(Receiver) 사이의 의존성을 제거하여 이벤트가 발생했을 때 실행될 기능이 다양하면서 변경이 필요한 경우 실행될 기능의 변경에도 호출자 클래스의 수정없이 그대로 사용할 수 있도록 해주는 패턴이다.

다음과 같은 램프를 켜는 버튼이 있다고 하자.

class Lamp {
    public void turnOn() {
        System.out.println("Lamp.turnOn");
    }
}

class Button {
    private Lamp lamp;
    public Button(Lamp lamp) {
        this.lamp = lamp;
    }
    public void pressed() {
        lamp.turnOn();
    }
}
  • 위 코드의 문제점
    • 버튼이 눌렸을 때 램프를 켜는 대신에 다른 기능을 수행하기 위해서 코드를 수정해야 한다.
      • 유지 보수의 어려움
      • OCP 위반

예를 들어 알람 기능을 추가하게 되면 다음과 같이 코드를 수정해야 한다.

enum Mode { LAMP, ALARM }

class Lamp {
    public void turnOn() {
        System.out.println("Lamp.turnOn");
    }
}

class Alarm {
    public void start() {
        System.out.println("Alarm.start");
    }
}

class Button {
    private Lamp lamp;
    private Alarm alarm;
    private Mode mode;
    public Button(Lamp lamp, Alarm alarm) {
        this.lamp = lamp;
        this.alarm = alarm;
    }
    public void setMode(Mode mode) {
        this.mode = mode;
    }
    public void pressed() {
        switch (mode) {
            case LAMP:
                lamp.turnOn();
                break;
            case ALARM:
                alarm.start();
                break;
        }
    }
}

기능을 변경하거나 새로운 기능을 추가할 때마다 Button 클래스를 수정해야 하므로 OCP를 위반한다.

이를 해결하기 위해 버튼이 눌렸을 때 수행될 기능을 캡슐화한다.

class Button {
    private Command command;
    public Button(Command command) {
        setCommand(command);
    }
    public void setCommand(Command command) {
        this.command = command;
    }
    public void pressed() {
        command.execute();
    }
}

interface Command {
    void execute();
}

class Lamp {
    public void turnOn() {
        System.out.println("Lamp.turnOn");
    }
}

class LampOnCommand implements Command {
    private Lamp lamp;
    public LampOnCommand(Lamp lamp) {
        this.lamp = lamp;
    }
    @Override
    public void execute() {
        lamp.turnOn();
    }
}

class Alarm {
    public void start() {
        System.out.println("Alarm.start");
    }
}

class AlarmStartCommand implements Command {
    private Alarm alarm;
    public AlarmStartCommand(Alarm alarm) {
        this.alarm = alarm;
    }
    @Override
    public void execute() {
        alarm.start();
    }
}

커맨드 패턴 컬레보레이션

Invoker : 기능의 실행을 요청하는 호출자 클래스

Command : 실행될 기능에 대한 인터페이스

ConcreteCommand : 실제로 실행되는 기능을 구현한 Command의 하위 클래스

Receiver : ConcreteCommand의 기능 실행을 위해 사용되는 수신자 클래스

 

 

 


커맨드 패턴을 이용한 간단한 예제

  • 게임기 설명 : 세 개의 버튼(각각 1개의 빨강, 초록, 파랑 버튼)을 사용하여 게임을 진행
  • 최초에 빨강 버튼을 누르면 게임 1, 초록 버튼을 누르면 게임 2, 파랑 버튼을 누르면 게임 3을 실행한다. 오락기는 3개의 게임을 제공한다. 각 게임에서 버튼의 기능은 다음과 같다.
    • 게임 1 : 주사위가 주사위고 또 주사위다(세 개의 주사위 중 가장 큰 값의 주사위를 고르는 게임. 세 개의 주사위가 생성되고, 숫자는 1~6 중에 랜덤으로 생성됨)
      • 빨강 버튼: 왼쪽 주사위 선택. 가장 큰 값이 아니라면 게임 오버
      • 초록 버튼: 가운데 주사위 선택. 가장 큰 값이 아니라면 게임 오버
      • 파란 버튼: 오른쪽 주사위 선택. 가장 큰 값이 아니라면 게임 오버
    • 게임 2 : 그쯤에서 재빨리 블록 빼내기(버튼 색과 동일한 색의 블록을 빼내는 게임. 블록의 개수는 10개이고, 색은 랜덤으로 생성됨)
      • 빨강 버튼: 가장 밑이 빨강 블록이라면 제거, 빨강 블록이 아니라면 게임오버
      • 초록 버튼: 가장 밑이 초록 블록이라면 제거, 초록 블록이 아니라면 게임오버
      • 파랑 버튼: 가장 밑이 파랑 블록이라면 제거, 파랑 블록이 아니라면 게임오버
    • 게임 3: 햄버거 가게의 당일치기 견습생(3개의 재료를 사용하여 햄버거를 만드는 게임. 햄버거의 내용물은 랜덤으로 생성됨)
      • 빨강 버튼: 토마토 추가. 토마토를 추가할 위치가 아니라면 게임 오버
      • 초록 버튼: 양상추 추가. 양상추를 추가할 위치가 아니라면 게임 오버
      • 파랑 버튼: 고기 패티 추가. 고기 패티를 추가할 위치가 아니라면 게임 오버
  • 구현 예시
[빨강버튼은 1을 입력, 초록버튼은 2를 입력, 파랑버튼은 3을 입력하세요]

게임을 선택하세요(게임1: 빨강버튼, 게임2: 초록버튼, 게임3: 파랑버튼)
2
게임2를 선택하였습니다.
블록과 동일한 색의 버튼을 누르는 게임입니다.
블록 순서: 빨강, 파랑, 초록, 초록, 빨강
버튼을 누르세요! 1
버튼을 누르세요! 3
버튼을 누르세요! 2
버튼을 누르세요! 1
게임 오버입니다. 게임을 종료합니다.

클래스 다이어그램

소스코드

import java.util.Set;
import java.util.HashSet;
import java.util.Random;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
        Arcade.getInstance().run();
    }
}

class Arcade {
    private static Arcade arcade = new Arcade();
    private String gameName;
    private boolean isOvered;

    private Arcade() {
        this.isOvered = false;
    }

    public static Arcade getInstance() {
        return arcade;
    }

    public void setGameName(String gameName) {
        this.gameName = gameName;
    }

    public void setIsOvered(boolean isOvered) {
        this.isOvered = isOvered;
    }

    public void run() {
        Scanner sc = new Scanner(System.in);
        boolean isFinished = false;
        int buttonNum;
        RedButton redButton = RedButton.getInstance();
        GreenButton greenButton = GreenButton.getInstance();
        BlueButton blueButton = BlueButton.getInstance();
        DiceGame diceGame = new DiceGame(this);
        BlockGame blockGame = new BlockGame(this);
        HamburgerGame hamburgerGame = new HamburgerGame(this);
        while (true) {
            System.out.println("[빨강버튼은 1을 입력, 초록버튼은 2를 입력, 파랑버튼은 3을 입력하세요]\n");
            System.out.println("게임을 선택하세요(게임1: 빨강버튼, 게임2: 초록버튼, 게임3: 파랑버튼)");
            buttonNum = sc.nextInt();
            switch (buttonNum) {
                case 1:
                    System.out.println("게임1을 선택하였습니다.");
                    redButton.pressed();
                    break;
                case 2:
                    System.out.println("게임2을 선택하였습니다.");
                    greenButton.pressed();
                    break;
                case 3:
                    System.out.println("게임3을 선택하였습니다.");
                    blueButton.pressed();
                    break;
                default:
                    System.out.println("게임을 종료합니다.");
                    isFinished = true;
            }
            if (isFinished) break;
            switch (gameName) {
                case "DiceGame":
                    redButton.setCommand(new LeftDiceCommand(diceGame));
                    greenButton.setCommand(new CenterDiceCommand(diceGame));
                    blueButton.setCommand(new RightDiceCommand(diceGame));
                    diceGame.start();
                    break;
                case "BlockGame":
                    redButton.setCommand(new RedBlockCommand(blockGame));
                    greenButton.setCommand(new GreenBlockCommand(blockGame));
                    blueButton.setCommand(new BlueBlockCommand(blockGame));
                    blockGame.start();
                    break;
                case "HamburgerGame":
                    redButton.setCommand(new TomatoCommand(hamburgerGame));
                    greenButton.setCommand(new CabbageCommand(hamburgerGame));
                    blueButton.setCommand(new PattyCommand(hamburgerGame));
                    hamburgerGame.start();
                    break;
            }
            if (gameName.equals("DiceGame")) {
                buttonNum = sc.nextInt();
                switch (buttonNum) {
                    case 1:
                        redButton.pressed();
                        break;
                    case 2:
                        greenButton.pressed();
                        break;
                    case 3:
                        blueButton.pressed();
                        break;
                }
            } else {
                for (int i = 0; i < 5; i++) {
                    buttonNum = sc.nextInt();
                    switch (buttonNum) {
                        case 1:
                            redButton.pressed();
                            break;
                        case 2:
                            greenButton.pressed();
                            break;
                        case 3:
                            blueButton.pressed();
                            break;
                    }
                    if (isOvered) break;
                }
            }
            setIsOvered(false);
            redButton.setCommand(RedSelectCommand.getInstance());
            greenButton.setCommand(GreenSelectedCommand.getInstance());
            blueButton.setCommand(BlueSelectedCommand.getInstance());
        }
        sc.close();
    }
}

class DiceGame {
    private Arcade arcade;
    private int leftDice, centerDice, rightDice, maxDice;
    private Set<Integer> numSet = new HashSet<>();

    public DiceGame(Arcade arcade) {
        this.arcade = arcade;
    }

    public void start() {
        System.out.println("세 개의 주사위 중 가장 큰 값의 주사위를 고르는 게임입니다.");
        System.out.println("빨강 버튼: 왼쪽 주사위 선택 / 초록 버튼: 가운데 주사위 선택 / 파란 버튼: 오른쪽 주사위 선택");
        Random random = new Random();
        leftDice = 1 + random.nextInt(6);
        numSet.add(leftDice);
        do {
            centerDice = 1 + random.nextInt(6);
        } while (numSet.contains(centerDice));
        numSet.add(centerDice);
        do {
            rightDice = 1 + random.nextInt(6);
        } while (numSet.contains(rightDice));
        numSet.add(rightDice);
        System.out.println("왼쪽 주사위: " + leftDice + ", 가운데 주사위: " + centerDice + ", 오른쪽 주사위: " + rightDice);
        maxDice = Math.max(Math.max(leftDice, centerDice), rightDice);
    }

    public void selectLeftDice() {
        if (leftDice == maxDice) {
            System.out.println("이겼습니다.");
        } else {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
        }
    }

    public void selectCenterDice() {
        if (centerDice == maxDice) {
            System.out.println("이겼습니다.");
        } else {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
        }
    }

    public void selectRightDice() {
        if (rightDice == maxDice) {
            System.out.println("이겼습니다.");
        } else {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
        }
    }
}

class BlockGame {
    private Arcade arcade;
    private String[] blocks;
    private int blockNum;
    private int cnt = 0;

    public BlockGame(Arcade arcade) {
        this.arcade = arcade;
    }

    public void start() {
        System.out.println("버튼 색과 동일한 색의 블록을 빼내는 게임입니다.");
        System.out.println("빨강 버튼: 가장 밑이 빨강 블록이라면 제거 / 초록 버튼: 가장 밑이 초록 블록이라면 제거 / 파란 버튼: 가장 밑이 파랑 블록이라면 제거");
        blocks = new String[5];
        Random random = new Random();
        System.out.print("블록 순서: ");
        for (int i = 0; i < 5; i++) {
            blockNum = 1 + random.nextInt(3);
            switch (blockNum) {
                case 1:
                    blocks[i] = "빨강";
                    break;
                case 2:
                    blocks[i] = "초록";
                    break;
                case 3:
                    blocks[i] = "파랑";
                    break;
            }
            System.out.print(blocks[i] + ", ");
        }
    }

    public void selectRedBlock() {
        if (!blocks[cnt++].equals("빨강")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }

    public void selectGreenBlock() {
        if (!blocks[cnt++].equals("초록")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }

    public void selectBlueBlock() {
        if (!blocks[cnt++].equals("파랑")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }
}

class HamburgerGame {
    private Arcade arcade;
    private String[] ingredients;
    private int ingredientNum;
    private int cnt = 0;

    public HamburgerGame(Arcade arcade) {
        this.arcade = arcade;
    }

    public void start() {
        System.out.println("3개의 재료를 사용하여 햄버거를 만드는 게임입니다.");
        System.out.println("빨강 버튼: 토마토 추가 / 초록 버튼: 양배추 추가 / 파란 버튼: 고기 패티 추가");
        ingredients = new String[5];
        Random random = new Random();
        System.out.print("블록 순서: ");
        for (int i = 0; i < 5; i++) {
            ingredientNum = 1 + random.nextInt(3);
            switch (ingredientNum) {
                case 1:
                    ingredients[i] = "토마토";
                    break;
                case 2:
                    ingredients[i] = "양배추";
                    break;
                case 3:
                    ingredients[i] = "고기 패티";
                    break;
            }
            System.out.print(ingredients[i] + ", ");
        }
    }

    public void selectTomato() {
        if (!ingredients[cnt++].equals("토마토")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }

    public void selectCabbage() {
        if (!ingredients[cnt++].equals("양배추")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }

    public void selectPatty() {
        if (!ingredients[cnt++].equals("고기 패티")) {
            System.out.println("게임 오버입니다. 게임을 종료합니다.");
            arcade.setIsOvered(true);
            cnt = 0;
        }
    }
}

class RedButton {
    private static RedButton redButton = new RedButton(RedSelectCommand.getInstance());
    private RedButtonCommand theCommand;

    private RedButton(RedButtonCommand theCommand) {
        this.theCommand = theCommand;
    }

    public static RedButton getInstance() {
        return redButton;
    }

    public void setCommand(RedButtonCommand newCommand) {
        this.theCommand = newCommand;
    }

    public void pressed() {
        this.theCommand.execute();
    }
}

interface RedButtonCommand {
    void execute();
}

class RedSelectCommand implements RedButtonCommand {
    private static RedSelectCommand redSelectCommand = new RedSelectCommand();
    private Arcade arcade = Arcade.getInstance();

    private RedSelectCommand() {}

    public static RedSelectCommand getInstance() {
        return redSelectCommand;
    }

    @Override
    public void execute() {
        arcade.setGameName("DiceGame");
    }
}

class LeftDiceCommand implements RedButtonCommand {
    private DiceGame diceGame;

    public LeftDiceCommand(DiceGame diceGame) {
        this.diceGame = diceGame;
    }

    @Override
    public void execute() {
        diceGame.selectLeftDice();
    }
}

class RedBlockCommand implements RedButtonCommand {
    private BlockGame blockGame;

    public RedBlockCommand(BlockGame blockGame) {
        this.blockGame = blockGame;
    }

    @Override
    public void execute() {
        blockGame.selectRedBlock();
    }
}

class TomatoCommand implements RedButtonCommand {
    private HamburgerGame hamburgerGame;

    public TomatoCommand(HamburgerGame hamburgerGame) {
        this.hamburgerGame = hamburgerGame;
    }

    @Override
    public void execute() {
        hamburgerGame.selectTomato();
    }
}

class GreenButton {
    private static GreenButton greenButton = new GreenButton(GreenSelectedCommand.getInstance());
    private GreenButtonCommand theCommand;

    private GreenButton(GreenButtonCommand theCommand) {
        this.theCommand = theCommand;
    }

    public static GreenButton getInstance() {
        return greenButton;
    }

    public void setCommand(GreenButtonCommand newCommand) {
        this.theCommand = newCommand;
    }

    public void pressed() {
        this.theCommand.execute();
    }
}

interface GreenButtonCommand {
    void execute();
}

class GreenSelectedCommand implements GreenButtonCommand {
    private static GreenSelectedCommand greenSelectedCommand = new GreenSelectedCommand();
    private Arcade arcade = Arcade.getInstance();

    private GreenSelectedCommand() {}

    public static GreenSelectedCommand getInstance() {
        return greenSelectedCommand;
    }

    @Override
    public void execute() {
        arcade.setGameName("BlockGame");
    }
}

class CenterDiceCommand implements GreenButtonCommand {
    private DiceGame diceGame;

    public CenterDiceCommand(DiceGame diceGame) {
        this.diceGame = diceGame;
    }

    @Override
    public void execute() {
        diceGame.selectCenterDice();
    }
}

class GreenBlockCommand implements GreenButtonCommand {
    private BlockGame blockGame;

    public GreenBlockCommand(BlockGame blockGame) {
        this.blockGame = blockGame;
    }

    @Override
    public void execute() {
        blockGame.selectGreenBlock();
    }
}

class CabbageCommand implements GreenButtonCommand {
    private HamburgerGame hamburgerGame;

    public CabbageCommand(HamburgerGame hamburgerGame) {
        this.hamburgerGame = hamburgerGame;
    }

    @Override
    public void execute() {
        hamburgerGame.selectCabbage();
    }
}

class BlueButton {
    private static BlueButton blueButton = new BlueButton(BlueSelectedCommand.getInstance());
    private BlueButtonCommand theCommand;

    private BlueButton(BlueButtonCommand theCommand) {
        this.theCommand = theCommand;
    }

    public static BlueButton getInstance() {
        return blueButton;
    }

    public void setCommand(BlueButtonCommand newCommand) {
        this.theCommand = newCommand;
    }

    public void pressed() {
        this.theCommand.execute();
    }
}

interface BlueButtonCommand {
    void execute();
}

class BlueSelectedCommand implements BlueButtonCommand {
    private static BlueSelectedCommand blueSelectedCommand = new BlueSelectedCommand();
    private Arcade arcade = Arcade.getInstance();

    private BlueSelectedCommand() {}

    public static BlueSelectedCommand getInstance() {
        return blueSelectedCommand;
    }

    @Override
    public void execute() {
        arcade.setGameName("HamburgerGame");
    }
}

class RightDiceCommand implements BlueButtonCommand {
    private DiceGame diceGame;

    public RightDiceCommand(DiceGame diceGame) {
        this.diceGame = diceGame;
    }

    @Override
    public void execute() {
        diceGame.selectRightDice();
    }
}

class BlueBlockCommand implements BlueButtonCommand {
    private BlockGame blockGame;

    public BlueBlockCommand(BlockGame blockGame) {
        this.blockGame = blockGame;
    }

    @Override
    public void execute() {
        blockGame.selectBlueBlock();
    }
}

class PattyCommand implements BlueButtonCommand {
    private HamburgerGame hamburgerGame;

    public PattyCommand(HamburgerGame hamburgerGame) {
        this.hamburgerGame = hamburgerGame;
    }
    @Override
    public void execute() {
        hamburgerGame.selectPatty();
    }
}
728x90

'JAVA객체지향디자인패턴' 카테고리의 다른 글

데커레이터(Decorator) 패턴  (0) 2023.07.09
옵서버(Observer) 패턴  (0) 2023.07.06
스테이트(State) 패턴  (0) 2023.07.06
스트래티지(Strategy) 패턴  (0) 2023.07.06
싱글턴(Singleton) 패턴  (0) 2023.07.05
Comments