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);
}
public boolean execute() {
editor.clipboard = editor.textField.getSelectedText();
return false;
}
public class PasteCommand extends Command {
public PasteCommand(Editor editor) {
super(editor);
}
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() {
public void actionPerformed(ActionEvent e) {
executeCommand(new CopyCommand(editor));
}
});
ctrlX.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
executeCommand(new CutCommand(editor));
}
});
ctrlV.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
executeCommand(new PasteCommand(editor));
}
});
ctrlZ.addActionListener(new ActionListener() {
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
Post a Comment