design pattern #13 Behavioral pattern - Command

- 툴박스에 쓸 버튼을 하나 만들었다고 치자. 이 버튼을 상속시켜, 여러가지 저장하기, 복사하기, 붙여넣기 등의 버턴을 만든다고 치자. 뭐가 문젠가?


- 문제가 충분히 될 수 있다. 일단, subclass 종류가 너무 많다. 그건 그렇다 치더라도, 복사하기가 버튼에만 있는게 아니고. 메뉴에도 있고, 단축기로도 가능하다. 단축기나 메뉴를 버튼에서 상속하자니 말이 안되고, 그렇지 않으려면 중복된 코드가 생기게 된다. 


- command 패턴을 레스토랑에 비교했다. 손님이 테이블에서 주문을 하면, 웨이터는 이를 받아 적어 주방 어딘가에 stick note 를 붙여 놓는다. 요리사는 이 목록대로 음식을 만들어서 다시 내놓고. 웨이터는 목록과 음식을 최종 확인 한 뒤 손님에게 가져다준다.


- 비즈니스로직과 GUI 분리의 관점에서, 위 상황의 버튼과 기능을 분리하는게 당연하지만, command 패턴이 말하는건 그냥 분리가 아니다. GUI 가 비즈리스 로직에 request 를 바로 보내는 것이 아니라, 따로 분리된 Command 라는 클래스에, 비즈니스 로직에 필요한 데이터들을 모아두고,  Command 라는 클래스에는 이 요청을 수행하는 method 하나가 존재한다. 그러면 GUI 가 비즈니스 로직에 대해 잘 몰라도, Command 만 트리거 하면 된다. 


- Command 클래스는 보통 parameters 가 없는 execute method 하나만 가진다.


- 그럼 명령 수행에 필요한 매개변수 같은건 어떻게 넘기는가? 아래 예제를 보면,  Command abstract class 에 생성자로  Editor 를 받는데, Editor 자체로 충분하다. 다른 경우에도, 생성자의 매개변수로 넘기고, execute() 함수에서 각 command 별 자세한 처리를 한다. 


- Command  패턴은 이렇듯 객체와 할 행동을 같이 묶어 객체로 넘기는데 사용되고, 객체로 넘기기 때문에 serialize 가 가능하고, reversible 한 작업을 할 수 있는데, 아래의 복붙의 경우 되돌리기를 구현 가능하다. Stack  등을 이용해서 말이다. 


public abstract class Command {
    public Editor editor;
    private String backup;

    Command(Editor editor) {
        this.editor = editor;
    }

    void backup() {
        backup = editor.textField.getText();
    }

    public void undo() {
        editor.textField.setText(backup);
    }

    public abstract boolean execute();
}
public class CopyCommand extends Command {

    public CopyCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        editor.clipboard = editor.textField.getSelectedText();
        return false;
    }

public class PasteCommand extends Command {

    public PasteCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;

        backup();
        editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
        return true;
    }
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Editor {
    public JTextArea textField;
    public String clipboard;
    private CommandHistory history = new CommandHistory();

    public void init() {
        JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
        JPanel content = new JPanel();
        frame.setContentPane(content);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        textField = new JTextArea();
        textField.setLineWrap(true);
        content.add(textField);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        JButton ctrlC = new JButton("Ctrl+C");
        JButton ctrlX = new JButton("Ctrl+X");
        JButton ctrlV = new JButton("Ctrl+V");
        JButton ctrlZ = new JButton("Ctrl+Z");
        Editor editor = this;
        ctrlC.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CopyCommand(editor));
            }
        });
        ctrlX.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CutCommand(editor));
            }
        });
        ctrlV.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new PasteCommand(editor));
            }
        });
        ctrlZ.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                undo();
            }
        });
        buttons.add(ctrlC);
        buttons.add(ctrlX);
        buttons.add(ctrlV);
        buttons.add(ctrlZ);
        content.add(buttons);
        frame.setSize(450, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void executeCommand(Command command) {
        if (command.execute()) {
            history.push(command);
        }
    }

    private void undo() {
        if (history.isEmpty()) return;

        Command command = history.pop();
        if (command != null) {
            command.undo();
        }
    }
}

Comments

Popular posts from this blog

삼성전자 무선사업부 퇴사 후기

개발자 커리어로 해외 취업, 독일 이직 프로세스

코드리뷰에 대하여