디자인 패턴 기말 정리
1. Decorator
- 장식물과 내용물을 동일시하기
- 중심이 되는 객체에, 장식과 같은 부가적인 기능들을 하나식 입혀서 좀 더 목적에 어울리는 객체를 만들자
1.1 예제 프로그램
- Display 클래스
- 복수행으로 구성되는 문자열을 표시하기 위한 추상 클래스
- getColumns()
가로의 문자 수를 얻기 위한 메소드 - getRows()
행의 수를 얻기 위한 메소드 - getRowText()
지정한 행의 문자열을 얻기 위한 메소드 - show()
모든 행을 화면에 표시하는 메소드
- TemplateMethod 패턴 적용
public abstract class Display {
public abstract int getColumns();
public abstract int getRows();
public abstract String getRowText(int row);
public void show() {
for (int i = 0; i < getRows(); i++) {
System.out.println(getRowText(i));
}
}
}
- StringDisplay 클래스
- 한줄의 문자열을 표시하는 클래스
- string 필드
- 표시할 문자열을 저장함
- getColumns()
- string.getBytes().length 를 사용하여 문자열이 차지하는 바이트 수를 반환함
- getRows()
- 1을 반환
- getRowText(int row)
입력 매개변수 row 가 0일 때만 string 필드를 반환한다. - 이 클래스는, 여러 케이크의 중심에 있는 스펀지 케이크에 해당함
public class StringDisplay extends Display {
private String string;
public StringDisplay(String string) {
this.string = string;
}
public int getColumns() {
return string.getBytes().length;
}
public int getRows() {
return 1;
}
public String getRowText(int row) {
if (row == 0) {
return string;
} else {
return null;
}
}
}
- Border 클래스
- '장식'을 나타내는 추상 클래스
- Display 의 하위 클래스로 정의됨
- 장식이 내용물과 동일한 메소드를 가진다.
- 장식과 내용물을 동일시 할 수 있다. : (장식과 내용물을 동일시 하기)
- 장식이 내용물이 될 수 있다는 것은 장식 클래스를 내용물로 해서 또 다른 장식을 붙일 수 있다는 의미
- display 필드: Display 형으로 선언됨
- 장식이 감사고 있는 "내용물"을 가리킨다.
- 이 필드는 StringDisplay 뿐만 아니라 Border 도 될 수 있다.
(이유: Border 도 Display 의 하위 클래스 이므로) - Decorator, Composite = 동일시 하기, 재귀적 패턴
- 상위 클래스와 동일한 함수를 제공한다는 것을 표현
- 생성자의 매개변수로 받은 객체를 활용하여 메소드를 구현
public abstract class Border extends Display {
protected Display display;
protected Border(Display display) {
this.display = display;
}
}
- SideBorder 클래스
- Border 의 하위 클래스
- 구체적인 장식의 일종
- 문자열 좌우에 정해진 문자로 장식한다.
- 생성자에서, 내용물(display)과 장식 문자(ch)가 지정됨
- getColumns()
내용물의 문자 수에 2를 더한다 - getRows()
내용물의 getRows()를 호출한다. - getRowText()
내용물의 Text 양쪽에 장식 문자를 연결하여 반환한다.
public class SideBorder extends Border {
private char borderChar;
public SideBorder(Display display, char ch) {
super(display);
this.borderChar = ch;
}
public int getColumns() {
return 1 + display.getColumns() + 1;
}
public int getRows() {
return display.getRows();
}
public String getRowText(int row) {
return borderChar + display.getRowText(row) + borderChar;
}
}
- FullBorder 클래스
- Border 의 하위 클래스
- 상하좌우에 장식을 한다.
- SideBorder와 달리, 장식할 문자가 미리 고정되어 있다.
- makeLine(char ch, int count)
ch 를 count 갯수 만큼 연속해서 문자열로 만드는 메소드 - getRowText(int row)
입력인자 row 가 0이거나 내용물의 전체 줄 수 + 1 과 같으면 문자열 상단 또는 하단에 장식할 문자열을 만든다.
public class FullBorder extends Border {
public FullBorder(Display display) {
super(display);
}
public int getColumns() {
return 1 + display.getColumns() + 1;
}
public int getRows() {
return 1 + display.getRows() + 1;
}
public String getRowText(int row) {
if (row == 0) { // 첫번째 줄 장식문자
return "+" + makeLine('-', display.getColumns()) + "+";
} else if (row == display.getRows() + 1) { // 마지막 줄 장식문자
return "+" + makeLine('-', display.getColumns()) + "+";
} else { 가운데 출력할 문자 양 옆에 문자 추가
return "|" + display.getRowText(row - 1) + "|";
}
}
private String makeLine(char ch, int count) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < count; i++) {
buf.append(ch);
}
return buf.toString();
}
}
1.2 등장 역할
- Component
- 기능을 추가할 때 핵심이 되는 역할
- Display 클래스
- ConcreteComponent
- Component 역할을 구현한 구체적인 클래스
- StringDisplay
- Decorator의 역할
- Component 역할과 동일한 인터페이스를 가짐
- 장식자이면서, 장식할 대상이 되기도 한다. (component 의 하위 클래스이면서 멤버변수)
- Border 클래스
- Concrete Decorator
- 구체적인 장식자
- SideBorder, FullBorder 클래스
- Border 클래스가 Display 클래스의 하위 클래스
- 장식을 나타내는 Border 클래스가, 내용물을 나타내는 Display 와 동일한 인터페이스를 가진다.
- 장식하는 클래스가, 다시 장식의 대상이 될 수 있다.
- Composite 과 마찬가지로 재귀적인 구조
- 목적이 서로 다르다.
- Composite 패턴
- container 가 다시 내용물이 될 수 있다.
- Composite 가 Component(Composite의 대상)가 될 수 있다.
- Decorator 패턴
- 장식하는 클래스가 다시 장식 대상이 될 수 있다.
- Decorator 가 Component 가 될 수 있다.
- 내용물을 변경하지 않고 기능을 추가할 수 있다.
- 내용물 변경 없이 새로운 장식을 계속해서 부착
- 포장되는 대상을 변경하지 않고 기능을 추가할 수 있다.
- Decorator 패턴에서는 위임이 사용된다.
- SideBorder 의 getColumns() 내에서는 display.getColumns()를 호출한다.
- 단순한 장식으로도 다양한 기능을 추가할 수 있다.
- 간단한 구체적인 장식을 많이 준비해두고, 그것들을 자유롭게 조합하여 새로운 장식을 만들 수 있다.
- 단점: 작은 객체를 여러개 만들어야 함 -> 메모리에 부담
- java.io 패키지와 Decorator 패턴
- 입출력 관련 패키지 java.io 에, Decorator 패턴이 사용됨
Reader = Display, FileReader(ConcreteComponent), BufferedReader(
- 파일로부터 데이터를 읽어들일 때
Reader reader = new FileReader("datafile.txt") - 버퍼링 기능 추가
Reader reader = new BufferedReader(new FileReader("datafile.txt")); - 생성자 밖에 계속 새로운 생성자가 추가됨
- 장식은 아니지만, 컴포넌트가 다시 새로운 데코레이터가 됨
- Composite == 컨테이너가 컨테이너 안에 들어감
- 줄번호 관리 기능 추가
Reader reader = new LineNumberReader(new BufferedReader(new FileReader("datafile.txt"))); - 줄 번호 관리 - 버퍼링 실행 안함
Reader reader = new LineNumberReader(new FileReader("datafile.txt")); - 또 다른 예시
Reader reader = new LineNumberReader(new BufferedReader( new InputStreamReader( socker.getInputStream()));
- 파일로부터 데이터를 읽어들일 때
- javax.swing.border 패키지에도, 화면에 표시되는 컴포넌트에 추가할 수 있는 장식용 클래스들이 모여있다.
-> Border를 씌운 컴포넌트에 다시 Border를 씌울 수 있다.
- 입출력 관련 패키지 java.io 에, Decorator 패턴이 사용됨
- Decorator 패턴을 사용하면, 유사한 작은 클래스들이 많아지는 단점이 있다.
- 이유: 하나의 큰 장식 대신, 작은 장식자들로 나누어지므로
1.3 보강
상속 - 하위 클래스와 상위 클래스의 동일시
- Child 인스턴스를 Parent 형 변수에 그대로 대입하고, Parent로부터 상속받은 메소드를 그대로 불러낼 수 있다. 하위 클래스를 상위 클래스로 간주
- 상위 클래스를 하위 클래스로 간주: 하위 클래스의 메소드를 불러내기 위해서는 type cast 가 필요함
Parent obj = new Child();
((Child) obj).childMethod(); - Parent 객체는 Child 객체로 형변환 할 수 없음
위임 - 자신과 위임할 곳을 동일시
- Rose 와 Violet 이 똑같은 메소드를 가지고 있고, Rose는 Violet에게 위임한다.
- '공통' 이란 정보가 보이지 않음 연결되어 있다는 정보를 알 수 없음
- Rose 와 Violet 이 똑같은 메소드인 method를 제공한다는 조건이 소스 코드에 표현되지 않음
- Flower라는 공통 상위 클래스를 정의하면, '공통 정보 공유'가 명확해진다.
1.4 연습문제
1) UpDownBorder 클래스
public class UpDownBorder extends Border {
private char borderChar;
public UpDownBorder(Display display, char ch) {
super(display);
this.borderChar = ch;
}
public int getColumns() {
return display.getColumns();
}
public int getRows() {
return 1 + display.getRows() + 1;
}
public String getRowText(int row) {
if (row == 0 || row == display.getRows() + 1) {
// display.getRows() -> 장식 전의 문자열
return makeLine(borderChar, getColumns());
} else {
return display.getRowText(row - 1);
}
}
private String makeLine(char ch, int count) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < count; i++) {
buf.append(ch);
}
return buf.toString();
}
}
2) MultiStringDisplay 클래스 (여러줄의 문자열 표시)
public class MultiStringDisplay extends Display {
private ArrayList<String> body = new ArrayList();
private int columns = 0;
public void add(String msg) {
body.add(msg);
updateColumn(msg);
}
public int getColumns() {
return columns;
}
public int getRows() {
return body.size();
}
public String getRowText(int row) {
return (String)body.get(row);
}
private void updateColumn(String msg) {
if (msg.getBytes().length > columns) { // 문자열들 중 길이가 가장 긴 값을 지정
columns = msg.getBytes().length;
}
for (int row = 0; row < body.size(); row++) {
int fills = columns - ((String)body.get(row)).getBytes().length;
if (fills > 0) {
body.set(row, body.get(row) + spaces(fills));
}
}
}
private String spaces(int count) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < count; i++) {
buf.append(' ');
}
return buf.toString();
}
}
2. Visitor
"데이터 구조 안을 돌아다니면서 처리를 수행하는 Visitor 패턴"
- 데이터 구조 안에 저장되어 있는 많은 요소에 대해서, 각 요소에 대해 무언가 "처리"해 나가고자 한다.
- 데이터 구조를 나타내고 있는 클래스 안에 "처리"를 기술해야 한다고 생각하면 X
- "처리"가 여러 종류라고 한다면
-> 새로운 처리가 필요해질 때마다, 데이터 구조를 나타내는 클래스를 수정해야 한다. -> 다른 방법 필요
- 데이터 구조와 처리를 분리
- 데이터 구조를 돌아다니는 "방문자"를 정의해서, 이 방문자가 "처리"를 담당하도록 함
- 새로운 처리를 추가하고 싶을 때는 -> 새로운 "방문자" 만든다.
- 데이터 구조는, 문을 두드리는 "방문자"를 받아들이기만 하면 된다.
- 방문자가 방문하는 데이터 구조
- Composite 패턴의 예제에서 사용된 파일과 디렉토리를 다시 사용
- 파일과 디렉토리로 구성된 데이터 구조를 방문자가 방문하면서, 파일 리스트를 출력하는 예제
2.1 예제 프로그램
코드
- Element 인터페이스
- 방문자를 받아들이는 클래스를 위한 인터페이스
- accept(Visitor v)
Visitor 타입을 입력 인자로 받아들임
- Entry 클래스
- Composite 패턴에 등장했던 File 이나 Directory 를 위한 추상 클래스
- Element 인터페이스를 구현함
- add()
Directory 클래스에서만 add() 가 유효하므로, Entry 클래스에서는 일단 에러로 처리한다. - iterator()
- 요소에 대한 Iterator를 얻을 때 호출되는 메소드
- Directory 클래스에서만 iterator() 가 유효하므로, Entry 클래스에서는 일단 에러로 처리한다.
(ArrayList의 iterator() 호출)
- File 클래스
- accept(Visitor v)
- 클라이언트가 File 객체에게 "방문자를 받아들이세요" 라고 요청할 때 호출하는 메소드
(클라이언트는 Visitor 를 매개변수로 하여 accept(v)를 호출할 것 - 입력 인자로 들어온 방문자의 visit 메소드를 호출한다.
- 이때, 현재 자신 객체를 인자로 하여 호출
- Visitor의 v.visit(File)메소드가 실행된다.
- 방문자가 방문하면 방문자에게 "나는 File 객체입니다. 나의 일을 처리해주세요"라고 요청하는 것과 비슷하다.
- 클라이언트가 File 객체에게 "방문자를 받아들이세요" 라고 요청할 때 호출하는 메소드
- accept(Visitor v)
- Directory 클래스
- iterator()
디렉토리가 유지하고 있는 엔트리들에 대한 Iterator 를 반환 - accept(Visitor)
- 입력 인자로 들어온 Visitor에게, 자기 자신을 매개 변수로 해서 v.visit()을 호출한다.
- 그러면, Visitor 의 visit(Directory) 메소드가 실행된다.
-> 방문자가 방문하면 방문자에게 "나는 Directory 객체입니다. 나의 일을 처리해 주세요"라고 요청하는 것과 비슷하다.
- iterator()
- FileTreatmentException 클래스
- File 엔트리에 무엇인가 추가 하고자 할 때 발생되는 예외
- Visitor 클래스
- 방문자를 나타내는 추상 클래스
- visit(File)
File 을 방문했을 때 File 클래스가 호출하는 메소드 - visit(Directory)
Directory 를 방문했을 때 Directory클래스가 호출하는 메소드 - 메소드 오버로드 이용
-> visit 메소드의 이름은 같은데, 인자의 종류가 다르다.
- ListVisitor 클래스
- Visitor 클래스의 하위 클래스
- 실제 데이터구조(File, Directory) 를 옮겨 다니면서, 리스트를 출력하는 일을 한다.
- currentdir 필드
- 현재 주목하고 있는 디렉토리 명을 저장함
- visit(File)
- 입력 인자로 받아들인 "File 에 대해 수행해야 할 처리" 가 기술되어 있다.
- 알고리즘
- 현재 디렉토리와 File의 toString() 반환 값을 연결하여 현재 파일의 전체 경로를 출력한다.
- visit(Directory)
- 입력 인자로 받아들인 "Directory 에 대해 수행해야 할 처리"가 기술되어 있다.
- 알고리즘
- 먼저, 디렉토리 전체 경로 및 크기를 출력한다.
- 현재 디렉토리를 임시로 savedir에 저장한다.
- 현재 디렉토리를, 입력 인자로 들어온 디렉토리로 변경한다. (방문할 디렉토리)
- 입력 인자로 들어온 디렉토리의 이터레이터를 얻는다.
- 입력 인자로 들어온 디렉토리가 가지는 원소들을 차례로 방문하면서, accept(this)를 호출하여 방문자가 방문했음을 알린다.
- while 루프가 끝나면, currentdir을 원래 디렉토리로 복귀시킨다
-> 계속 자기 자신을 넘겨주기 때문에, 값이 변경되면 전체 값에 영향
- 복잡한 재귀적인 호출
- accept메소드는 visit 메소드를 호출하고, visit 메소드는 accept 메소드를 호출한다.
- accept() { visit() -> accept() }
package Sample;
public class Main {
public static void main(String[] args) {
try {
System.out.println("Making root entries...");
Directory rootdir = new Directory("root");
Directory bindir = new Directory("bin");
Directory tmpdir = new Directory("tmp");
Directory usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
bindir.add(new File("vi", 10000));
bindir.add(new File("latex", 20000));
rootdir.accept(new ListVisitor());
System.out.println("");
System.out.println("Making user entries...");
Directory Kim = new Directory("Kim");
Directory Lee = new Directory("Lee");
Directory Park = new Directory("Park");
usrdir.add(Kim);
usrdir.add(Lee);
usrdir.add(Park);
Kim.add(new File("diary.html", 100));
Kim.add(new File("Composite.java", 200));
Lee.add(new File("memo.tex", 300));
Park.add(new File("game.doc", 400));
Park.add(new File("junk.mail", 500));
rootdir.accept(new ListVisitor());
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
- Main 클래스
- Composite 패턴
- main() 메소드가 엔트리 리스트를 출력하기 위해서 rootdir.printList() 를 호출
- 엔트리의 내용을 출력하는 책임을, File 이나 Directory 클래스가 가지고 있다.
- Visitor 패턴
- main() 메소드가 엔트리 리스트를 출력하기 위해서, rootdir.accept(new ListVisitor()) 를 호출하였다.
- 엔트리의 내용을 출력하는 책임을, ListVisitor 클래스가 가지고 있다.
- Composite 패턴
- 방문한 곳이 디렉토리이면, 현재 디렉토리 경로 및 크기를 출력하고,
각 내용물에 대해서 방문자를 받아들이라고 요청한다. - 방문한 곳이 파일이면, 현재 디렉토리와 파일 크기를 출력한다.
2.2 등장 역할
- Visitor 의 역할
- 데이터 구조 내의 각각의 구체적인 요소(ConcreteAcceptor 역할) 에 visit(xxx) 메소드를 선언하는 역할
- visit(xxx) 메소드는, 실제 xxx 를 처리하기 위한 실제 코드가 하위 클래스에 의해 제공된다.
- Visitor 클래스
- ConcreteVisitor의 역할
- Visitor 역할의 인터페이스를 실제로 구현하는 역할
- ListVisitor 클래스
- Acceptor 역할
- Visitor 역할이 방문할 장소를 나타내는 역할
- 방문자를 받아들이는 accept(Visitor)메소드를 선언한다.
- Element 인터페이스
- ConcreteAcceptor
- Acceptor 역할의 인터페이스를 구현하는 역할
- File, Directory 클래스
- ObjectStructures (객체의 구조)역할
- Acceptor 역할을 집합으로 취급할 수 있도록 해주는 메소드를 제공한다.
- 예제에서는 Directory 클래스가 해당됨
가지고 있는 요소들을 얻어갈 수 있도록, iterator 메소드를 제공함
2.3 힌트
- 더블 디스패치
- 디스패치: 급파하다, 발송하다, 신속히 처리하다.
- Visitor 와 Acceptor는 서로 대응 관계
ConcreteAcceptor 와 ConcreteVisitor의 역할을 하는 한 쌍에 의해 실제의 처리가 결정된다.
- 복잡한 일을 하는 이유
- Visitor 패턴의 목적
"처리"를 "데이터 구조"로부터 분리하는 것 - 다른 처리를 하는 ConcreteVisitor를 추가할 수 있다.
- 기존의 ConcreteVisitor의 기능을 확장하기 쉽다.
- 부품의 독립성을 높여준다.
- Visitor 패턴의 목적
- Open-Closed Principle
- 확장에 대해서는 열려있고
(클래스 설계 시 특별한 이유가 없는 한 장래의 확장을 허락해야 한다.) - 수정에 대해서는 닫혀있다.
(확장을 하더라도 기존의 클래스는 수정할 필요가 없어야 한다.)
- 확장에 대해서는 열려있고
- ConcreteVisitor 추가는 간단하지만, ConcreteElement 추가는 어렵다.
-> Element 구조가 바뀌면 이를 처리하는 모든 Visitor의 내부도 변경되어야 함
2.4 연습문제
1) FileFindVisitor 클래스 추가
public class FileFindVisitor extends Visitor {
private String filetype;
private ArrayList found = new ArrayList();
public FileFindVisitor(String filetype) {
this.filetype = filetype;
}
public Iterator getFoundFiles() {
return found.iterator();
}
public void visit(File file) {
if (file.getName().endsWith(filetype)) {
found.add(file);
}
}
public void visit(Directory directory) {
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
entry.accept(this);
}
}
}
- rootdir.accept(FileFindVisitor)
- visitor 가 방문해서 File 타입인 경우, 파일 확장자가 일치하면 found 리스트에 파일을 추가
- getFoundFiles() 로 이터레이터를 받아 찾은 파일들을 확인할 수 있다.
- endsWith()
특정 문자열을 매개변수로 받아 그 문자열로 끝나는지 여부 반환 - lastIndexOf()
해당 함수를 호출한 문자열의 뒤부터 탐색하여 매개변수로 받은 문자열이 가장 처음 나오는 인덱스를 반환 - substring(start_ind, end_ind)
문자열을 자르는 메소드 - equals()
String 값을 비교하는 메소드
2) SizeVisitor 클래스 비교하기
public class SizeVisitor extends Visitor {
private int size = 0;
public int getSize() {
return size;
}
public void visit(File file) {
size += file.getSize();
}
public void visit(Directory directory) {
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
entry.accept(this);
}
}
}
- dir이면 내부를 순회하고, File 이면 file 사이즈 추가
Directory
public int getSize() {
SizeVisitor v = new SizeVisitor();
accept(v);
return v.getSize();
}
- getSize() 를 호출하면 size가 계산되도록 수정
- 해당 파일사이즈 / 디렉토리 하위에 포함되는 파일 크기의 합이 저장됨
3) ElementArrayList 클래스 작성
ArrayList에 Element 인터페이스 기능을 가진 클래스
class ElementArrayList extends ArrayList implements Element {
public void accept(Visitor v) {
Iterator it = iterator();
while (it.hasNext()) {
Element e = (Element)it.next();
e.accept(v);
}
}
}
- ArrayList 를 상속받았기 때문에 ElementArrayList 는 ArrayList
- iterator() 함수가 ArrayList 에 있기 때문에 가져와서 순회할 수 있음
4) final 이 선언된 클래스는 하위 클래스를 만들 수 없다.
- String 클래스
- Open-Closed Principle 을 위반하고 있다.
- java 시스템은, String 클래스는 확장이 되지 않는다는 전제하에 스피드 향상이나 메모리 양을 위한 최적화를 실행하고 있기 때문에 위반함
3. Facade 패턴
- 객체 지향 프로그램은 많은 클래스와 객체들로 이루어지고, 서로 복잡한 관련을 맺고 있다.
- 이들을 모두 이해하고 제어하기 힘듦
- 이들을 제어하기 위한 '창구'역할을 담당하는 클래스를 만들자
- facade: 건물의 정면
- 복잡한 내부는 숨기고, 높은 레벨의 인터페이스를 외부에 제공한다.
3.1 예제 프로그램
- 사용자의 웹페이지를 작성하는 프로그램
- Facade 패턴의 예를 보이기 위해서는 "복잡하게 얽혀 있는 많은 클래스"가 필요
패키지명 | 이름 | 해설 |
Anonymous | Main | 동작 테스트용 클래스 |
pagemaker | Database | 메일 주소로부터 사용자명을 얻는 클래스 |
HtmlWriter | HTML 파일을 작성하는 클래스 | |
PageMaker | 메일 주소로부터 사용자의 웹페이지를 작성하는 클래스 |
클래스 다이어그램
import pagemaker.PageMaker;
public class Main {
public static void main(String[] args) {
PageMaker.makeWelcomePage("youngjin@youngjin.com", "welcome.html");
}
}
Main
- pagemaker 패키지의 PageMaker 클래스를 이용해서, youngjin@youngjin.com 를 key 로 한 value를 가지고 "welcome.html"이라는 HTML 문서를 완성한다.
- Main 클래스는 Database나 HtmlWriter 클래스를 직접 이용하지 않고, high-level API 를 직접 제공하는 PageMaker 의 메소드만을 사용하여 원하는 작업을 한다.
package pagemaker;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
public class PageMaker {
private PageMaker() {
}
// 사용자의 이름을 찾기 위한 키값: 메일 주소, 작성할 html 파일 이름
public static void makeWelcomePage(String mailaddr, String filename) {
try {
Properties mailprop = Database.getProperties("maildata"); // 파일 읽어들이기
String username = mailprop.getProperty(mailaddr); // key: mailaddr
HtmlWriter writer = new HtmlWriter(new FileWriter(filename)); // html 파일 작성
writer.title("Welcome to " + username + "'s page!");
writer.paragraph(username + "의 페이지에 오신걸 환영합니다.");
writer.paragraph("메일을 기다리고 있습니다.");
writer.mailto(mailaddr, username);
writer.close();
System.out.println(filename + " is created for " + mailaddr + " (" + username + ")");
} catch (IOException e) {
e.printStackTrace();
}
}
}
PageMaker
- 퍼싸드 역할
- Database 클래스와 HtmlWriter 클래스를 조합하여, 지정된 사용자의 웹 페이지를 작성하기 위한 클래스
- makeWelcomePage(String mailaddr, String filename)
- Database 클래스
maildata.txt 파일로부터 속성 집합 읽음 -> 속성 중에서 입력 인자로 들어온 주소를 키값으로 하여 value(이름)를 얻음 - HtmlWriter 클래스
입력 인자로 들어온 filename 파일에 HTML 문서를 작성
- Database 클래스
import java.io.Writer;
import java.io.IOException;
public class HtmlWriter {
private Writer writer;
public HtmlWriter(Writer writer) {
this.writer = writer;
}
public void title(String title) throws IOException {
writer.write("<html>");
writer.write("<head>");
writer.write("<title>" + title + "</title>");
writer.write("</head>");
writer.write("<body>\n");
writer.write("<h1>" + title + "</h1>\n");
}
public void paragraph(String msg) throws IOException {
writer.write("<p>" + msg + "</p>\n");
}
public void link(String href, String caption) throws IOException {
paragraph("<a href=\"" + href + "\">" + caption + "</a>");
}
public void mailto(String mailaddr, String username) throws IOException {
link("mailto:" + mailaddr, username);
}
public void close() throws IOException {
writer.write("</body>");
writer.write("</html>\n");
writer.close();
}
}
HtmlWriter
- 간단한 웹 페이지를 작성하는 클래스
- 생성자
Writer 타입의 인스턴스를 받아서 사용 - title()
헤더 및 타이틀에 대한 html 태그를 작성하는 메소드 - paragraph()
문단을 작성하는 메소드 - link()
하이퍼링크를 만드는 메소드 - mailto()
메일주소 링크를 만드는 메소드
link() 호출 - close()
HTML 출력 마무리, 끝냄 - 제약조건
title() 이 제일 먼저 호출되어야 함
package pagemaker;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class Database {
private Database() {
}
public static Properties getProperties(String dbname) {
String filename = dbname + ".txt";
Properties prop = new Properties();
try {
prop.load(new FileInputStream(filename));
} catch (IOException e) {
System.out.println("Warning: " + filename + " is not found.");
}
return prop;
}
}
Database
- 데이터베이스 명을 지정하면, 그 이름에 해당하는 프로퍼티 파일을 열어서 Properties 를 만드는 클래스
- properties.load(File)
key=value 로 파싱해서 데이터 가져오기 -> properties 형태
- properties.load(File)
- getProperties(String dbname)
- dbname.txt 파일로부터 여러 속성 값을 읽어 들여 Properties인스턴스를 생성한 후, 이를 리턴하는 메소드
- Properties extends Hashtable<Object, Object>
key 와 value 쌍으로 된 속성의 집합을 유지하는 클래스
- maildata.txt
속성 저장한 파일
key=value
-> 이름 가져오는데 사용
3.2 등장 역할
- Facade 역할
- 시스템을 구성하는 많은 역할의 '간단한 창구'가 되는 역할
- 높은 레벨에서 간단한 인터페이스를 시스템 외부에 제공
- PageMaker 클래스
- 서브시스템을 구성하고 있는 역할
- Facade 역할로부터 호출되는 서브시스템을 구성하는 클래스들
- Facade 역할을 의식하고 만들지는 않음
- Database, HtmlWriter 클래스
- Client
Facade 패턴을 이용하는 역할
Main 클래스
3.3 힌트
- Facade 역할이 하는 일은
- 복잡한 것을 단순하게 보여준다
- 내부에서 작동하고 있는 많은 클래스들의 관계나 사용법을 의식하지 않도록 해주는 역할
- 외부에 보이는 API를 적게 해주고 단순하게 해준다.
- API 가 적다는 것은 외부와 결합이 약해진다는 의미
- 이는 패키지를 부품으로 재이용하기 쉽다는 의미
- 패키지 설계 시에 어떤 클래스를 public 으로 할지 생각
- pagemaker 클래스의 대표인 Facade) PageMaker 만 public 으로 하고 나머지는 default 접근자로 설정
-> 외부 패키지에서 접근할 수 없게 막음
- 재귀적인 Facade 패턴의 적용
- 여러 패키지들이 있고, 패키지마다 Facade 역할이 정의되어 있다면, 이들 Facade 들에 대한 Facade 역할을 재귀적으로 정의할 수 있다.
3.4 예제프로그램
1) 외부에서 절대로 Database 클래스나 HtmlWriter 클래스를 이용할 수 없도록 하려면,
- 두 클래스 앞의 public 키워드 없앰
- deafult 접근자 -> 패키지 내부에서만 접근 가능
2) 메일 주소 링크만을 포함하는 파일을 만드는 makeLinkPage() 메소드를 PageMaker 클래스에 추가하기
public static void makeLinkPage(String filename) {
try {
HtmlWriter writer = new HtmlWriter(new FileWriter(filename));
writer.title("Link page");
Properties mailprop = Database.getProperties("maildata");
Enumeration en = mailprop.propertyNames();
while (en.hasMoreElements()) {
String mailaddr = (String)en.nextElement();
String username = mailprop.getProperty(mailaddr, "(unknown)");
writer.mailto(mailaddr, username);
}
writer.close();
System.out.println(filename + " is created.");
} catch (IOException e) {
e.printStackTrace();
}
}
- maildata.txt 내용을 이용해서 메일 주소 링크 만을 포함하는 파일을 만드는 makeLinkPage() 메소드를 PageMaker 클래스에 추가
- properties.propertyNames() => Enumeration 리턴됨
키 값이 전부 들어있는 데이터 - Enumeration -> 모든 키 값들이 들어있음
- en.hasMoreElements()
다음 원소가 있는지 체크 - en.nextElement()
다음 원소를 반환
- en.hasMoreElements()
- properties.getProperty("key", "default");
key: value 가져올 키 값
deafult: 키값에 해당하는 값이 없으면 사용
4. Observer
- 관찰자
- 관찰 대상의 상태가 변하면, 관찰자에게 통지된다.
- 객체의 상태 변화에 따른 처리를 기술할 때 유용하게 사용된다.
- 숫자 여러개를 생성하는 객체를 관찰자가 관찰해서, 그 값을 표시하는 프로그램 작성
- 관찰자의 종류에 따라 표시 방법이 다르다
- DigitObserver: 값을 숫자로 표시함
- GraphObserver: 값을 간이 그래프로 표시함
- 관찰대상인 RandomNumberGenerator 객체가 난수를 하나 발생,
- 등록된 모든 관찰자의 update 메소드를 호출해서 알려줌
이름 | 해설 |
Observer | 관찰자를 나타내는 인터페이스 |
NumberGenerator | 수를 생성하는 객체를 나타내는 추상 클래스 |
RandomNumberGenerator | 랜덤하게 수를 생성하는 클래스 |
DigitObserver | 숫자로 수를 표시하는 클래스 |
GraphObserver | 간이 그래프로 수를 표시하는 클래스 |
Main | 동작 테스트용 클래스 |
4.1 예제
import java.util.ArrayList;
import java.util.Iterator;
public abstract class NumberGenerator {
private ArrayList observers = new ArrayList();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void deleteObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
Iterator it = observers.iterator();
while (it.hasNext()) {
Observer o = (Observer)it.next();
o.update(this);
}
}
public abstract int getNumber();
public abstract void execute();
}
NumberGenerator
- 수를 생성하는 클래스
- execute(), getNumber() 추상 메소드
- observers 필드: NumberGenerator 를 관찰하고 있는 Observer 들을 보관하는 필드
- addObserver(observer)
Observer 를 추가할 때 호출하는 메소드 - deleteObserver(Observer)
Observer를 삭제할 때 호출하는 메소드 - notifyObservers()
- Observer 전원에게 "나의 내용이 갱신되었기 때문에 당신의 표시도 갱신해라"고 알려주는 메소드
- Observer 들의 upddate(this) 메소드를 차례차례 호출한다.
import java.util.Random;
public class RandomNumberGenerator extends NumberGenerator {
private Random random = new Random();
private int number;
public int getNumber() {
return number;
}
public void execute() {
for (int i = 0; i < 20; i++) {
number = random.nextInt(50);
notifyObservers();
}
}
}
RandomNumberGenerator
- NumberGenerator 의 하위 클래스
- 난수를 생성한다
java.util.Random 클래스 이용 - number 필드: 생성된 난수를 저장하는 변수
- execute()
- 0 ~ 49 까지의 난수 20개를 생성하고, 그때마다 notifyObservers 를 호출하여 관찰자들에게 통지한다.
- Random 클래스의 nextInt() 사용
public class DigitObserver implements Observer {
public void update(NumberGenerator generator) {
System.out.println("DigitObserver:" + generator.getNumber());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
DigitObserver
- Observer 인터페이스를 구현한 구체적인 관찰자
- 관찰한 수를 숫자로 표시함
- update(NumberGenerator)
- 인자로 전달된 NumberGenerator의 getNumber를 이용하여 수를 얻어서 화면에 출력
- 출력한 후, 표시된 모습을 잘 알 수 있도록 Thread.sleep() 을 이용하여 스피드를 늦춘다.
- Thread.sleep(100)
(100/1000 = 0.1 초) 동안 CPU 를 사용하지 않고 쉼
public class GraphObserver implements Observer {
public void update(NumberGenerator generator) {
System.out.print("GraphObserver:");
int count = generator.getNumber();
for (int i = 0; i < count; i++) {
System.out.print("*");
}
System.out.println("");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
GraphObserver
- Observer 인터페이스를 구현한 구체적인 관찰자
- 관찰한 수를 '간단한 그래프'로 표시함
- 관찰한 숫자 만큼의 '*'를 출력한다.
public class Main {
public static void main(String[] args) {
NumberGenerator generator = new RandomNumberGenerator();
Observer observer1 = new DigitObserver();
Observer observer2 = new GraphObserver();
generator.addObserver(observer1);
generator.addObserver(observer2);
generator.execute();
}
}
Main
- RandomNumberGenerator 인스턴스 1개 생성
- 관찰자 2개 생성해서 등록
- execute() 를 사용해서 수를 생성,
난수가 발생될 때마다 관찰자들에게 통지됨
4.2 등장 역할
- Subject (관찰 대상자 역할)
- 관찰되는 쪽을 나타냄
- Observer 역할을 등록하는 메소드 & 삭제하는 메소드 가짐
- 현재의 상태를 얻어갈 때 호출하는 메소드도 제공함
- NumberGenerator 인터페이스가 해당됨
- ConcreteSubject (구체적인 관찰 대상자 역할)
- 구체적인 '관찰되는 쪽'을 나타냄
- 상태가 변하면, 등록된 Observer 역할에게 통보함
- RandomNumberGenerator
- Observer (관찰자 역할)
- 관찰하는 쪽
- Subject 역할로부터 상태 변화를 통보받는 역할
- 예제에서는, Observer 인터페이스가 해당됨
- 통보받을 때, 관찰자의 update() 메소드가 호출
- ConcreteObserver (구체적인 관찰자 역할)
- 구체적인 관찰하는 쪽
- 상태 변화가 관찰 대상으로부터 통보되면 (update() 메소드가 호출되면, 그 메소드 안에서 Subject 역할의 현재 상태(getNumber())를 얻어서 적당한 일을 수행한다.
- DigitObserver, GraphObserver
4.3 힌트
- 교환 가능성 - 교체 용이성
- RandomNumberGenerator 클래스
- 자신을 관찰하는 Observer 가 실제로 어떤 클래스의 인스턴스인지 모름
- DigitObserver 클래스
- 자신이 관찰하고 있는 것이 어떤 인스턴스인지 신경쓰지 않음
- 관찰 대상이 NumberGenerator 의 하위 클래스이고 getNumber 메소드를 가지고 있다는 것만 알고있다.
- 관찰자와 관찰 대상을 논리적으로 분리 => 각각 쉽게 교체 가능
- 확장성 / 교환 가능성이 높아진다.
- RandomNumberGenerator 클래스
- 갱신을 위한 힌트 정보의 취급
- NumberGenerator 는 update() 메소드를 사용해서 '갱신되었습니다'라고 Observer 에게 통지한다.
- Observer 클래스의 update() 메소드의 파라미터는 NumberGenerator
- Observer 는 입력인자로 들어온 NumberGenerator를 이용하여 원하는 정보 추출
void update(NumberGenerator generator) {
generator.getNumber() 이용
} - NumberGenerator가, 관찰자가 필요한 정보만 넘겨줄 수도 있다. (시간 줄임)
void update(NumberGenerator generator, int number) {
//number
} - 관찰 대상이, 관찰자가 필요한 정보를 넘겨줄 수 있어야 한다는 부담
void update(int number) {
// number 이용
}
- NumberGenerator 는 update() 메소드를 사용해서 '갱신되었습니다'라고 Observer 에게 통지한다.
- 실제 동작 형태 => 통지
- Observer 의 역할이, 사실은 Subject로부터 통지가 오기를 기다리는 수동적인 역할을 한다.
- 그래서 Observer 패턴을 Publish(발행) - Subscribe(구독) 패턴이라고도 한다.
- Model / View / Controller (MVC)
- Smalltalk 언어에서, 하나의 데이터 모델을 여러 형태로 보여주고자 할 때 사용되는 유명한 패턴이다.
- Model 과 View 의 관계는, Observer 패턴에서 Subject와 Observer 의 역할과 서로 대응된다.
Model 의 데이터 값에 따라 observer 가 보여주는게 변함
- public interface Observer
- void update(Observable o, Object arg)
- 사용하기 쉽지 않음
Subject 클래스가 다른 클래스의 하위 클래스인 경우, Observable 의 하위 클래스로 할 수 없다.
- 다중상속 지원 안함
* Observer: 객체의 상태 변화를 다른 객체에게 통지하는 패턴 *
4.4 연습문제
1)
IncrementalNumberGenerator (연습문제-1)
public class IncrementalNumberGenerator extends NumberGenerator {
private int number;
private int end;
private int inc;
public IncrementalNumberGenerator(int start, int end, int inc) {
this.number = start;
this.end = end;
this.inc = inc;
}
public int getNumber() {
return number;
}
public void execute() {
while (number < end) {
notifyObservers();
number += inc;
}
}
}
- 시작하는 수와 마지막 수 및 증가분을 입력받는 생성자를 가진다.
10 ~ 50 까지 5 씩 증가 - 숫자가 만들어질 때마다 관찰자에게 통지한다.
2) 새로운 Observer 추가 - 그래픽 옵저버
FrameObserver (연습문제-2)
import java.awt.Frame;
import java.awt.TextField;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Button;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JTextField;
import java.awt.event.ActionEvent;
public class JFrameObserver extends JFrame implements Observer, ActionListener {
private GraphText textGraph = new GraphText(60);
private GraphCanvas canvasGraph = new GraphCanvas();
private Button buttonClose = new Button("Close");
public JFrameObserver() {
super("FrameObserver");
setLayout(new BorderLayout());
setBackground(Color.lightGray);
textGraph.setEditable(false);
canvasGraph.setSize(500, 500);
add(textGraph, BorderLayout.NORTH);
add(canvasGraph, BorderLayout.CENTER);
add(buttonClose, BorderLayout.SOUTH);
buttonClose.addActionListener(this);
pack(); // 내용에 맞게 창 크기 조절
// show(); show 는 deprecated 됨
setVisible(true); // 대체
}
public void actionPerformed(ActionEvent e) {
System.out.println(e.toString());
System.exit(0);
}
public void update(NumberGenerator generator) {
textGraph.update(generator);
canvasGraph.update(generator);
}
}
class GraphText extends JTextField implements Observer {
public GraphText(int columns) {
super(columns);
}
public void update(NumberGenerator generator) {
int number = generator.getNumber();
String text = number + ":";
for (int i = 0; i < number; i++) {
text += '*';
}
setText(text);
}
}
class GraphCanvas extends Canvas implements Observer {
private int number;
public void update(NumberGenerator generator) {
number = generator.getNumber();
repaint();
}
public void paint(Graphics g) {
int width = getWidth();
int height = getHeight();
g.setColor(Color.white);
g.fillArc(0, 0, width, height, 0, 360);
g.setColor(Color.red);
g.fillArc(0, 0, width, height, 90, - number * 360 / 50);
}
}
- g.fillArc(x, y, width, height, startAngle, arcAngle)
- startAngle
시작 각도
0: 3시 위치
(반 시계방향으로 증가 -> 90: 12시 위치) - arcAngle
- 양수
반시계방향 - 음수
시계방향
- 양수
- -360 * number / 50
각도 계산
- startAngle
5. State
- 어떤 것을 클래스로 표현할지는 설계하는 사람의 마음
- State 패턴 => 상태를 클래스로 표현한 것
- 클래스를 교체함으로써, '상태의 변화'를 나타낼 수 있고
- 새로운 상태를 추가해야 할 때 무엇을 프로그램하면 되는지 명확해짐
- 금고 경비 시스템
- 시각마다 경비 상태가 변화하는 금고 경비 시스템
- 호출 상황을 화면에 표시한다.
- 프로그램 상의 1초 == 현실 세계의 1시간으로 가정
- State 패턴을 사용하면, 현재 상태를 체크하기 위한 if 문이 없다.
- 묻혀 있던 상태를 외부로 끌어냈기 때문에
5.1 예제 프로그램
State | 금고의 상태를 나타내는 인터페이스 |
DayState | State 를 구현하고 있는 클래스. 주간의 상태를 나타냄 |
NightState | State 를 구현하고 있는 클래스. 야간의 상태를 나타냄 |
Context | 금고의 상태변화를 관리하고, 경비센터와 연락을 취하는 인터페이스 전체 시스템을 이용하는 인터페이스 |
SafeFrame | Context를 구현하고 있는 클래스 버튼이나 화면 표시 등의 사용자 인터페이스를 가짐 |
Main | 동작 테스트용 클래스 |
public interface State {
public abstract void doClock(Context context, int hour);
public abstract void doUse(Context context);
public abstract void doAlarm(Context context);
public abstract void doPhone(Context context);
}
State
- 금고의 상태를 나타냄
- 다음 이벤트가 발생했을 때 호출되는 인터페이스를 제공
- 시각 설정 => doClock() 을 호출
- 금고가 사용될 때 => doUse() 를 호출함
- 비상벨이 울릴 때 => doAlarm() 을 호출
- 일반 통화를 할 때 => doPhone() 을 호출
- 각 메소드의 형식 인자 Context
- 상태를 관리하거나 실제 경비센터를 호출하는 일을 하는 클래스
public class DayState implements State {
private static DayState singleton = new DayState();
private DayState() {
}
public static State getInstance() {
return singleton;
}
public void doClock(Context context, int hour) {
if (hour < 9 || 17 <= hour) {
context.changeState(NightState.getInstance());
}
}
public void doUse(Context context) {
context.recordLog("금고사용(주간)");
}
public void doAlarm(Context context) {
context.callSecurityCenter("비상벨(주간)");
}
public void doPhone(Context context) {
context.callSecurityCenter("일반통화(주간)");
}
public String toString() {
return "[주간]";
}
}
DayState
- 주간의 상태를 나타내는 클래스
- 하나의 상태만 필요, Singleton 패턴 사용
- DayState 타입의 객체를 static 으로 선언하고 인스턴스 한개 생성
- 생성자를 private
- doClock() 시각을 설정하는 메소드
- 인자에서 제공된 시각이 야간의 시각이면, 시스템의 상태를 야간으로 바꾼다.
- "주간 상태" 에서 하는 일을 표현하는 메소드 (Context 의 메소드를 이용)
- doUse() : 주간에 금고를 사용했을 때를 기록
- doAlarm() : 비상벨로 경비센터를 호출
- doPhone() : 경비 센터에 일반 통화
public class NightState implements State {
private static NightState singleton = new NightState();
private NightState() {
}
public static State getInstance() {
return singleton;
}
public void doClock(Context context, int hour) {
if (9 <= hour && hour < 17) { // 상태를 변경해야 하는지 시간을 체크
context.changeState(DayState.getInstance());
}
}
public void doUse(Context context) {
context.callSecurityCenter("비상: 야간의 금고사용!");
}
public void doAlarm(Context context) {
context.callSecurityCenter("비상벨(야간)");
}
public void doPhone(Context context) {
context.recordLog("야간의 통화녹음");
}
public String toString() {
return "[야간]";
}
}
NightState 클래스
- 야간의 상태를 나타내는 클래스
public interface Context {
public abstract void setClock(int hour);
public abstract void changeState(State state);
public abstract void callSecurityCenter(String msg);
public abstract void recordLog(String msg);
}
Context 클래스
- 상태를 관리하거나 경비센터를 실제로 호출하는 클래스를 위한 인터페이스를 제공
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class SafeFrame extends Frame implements ActionListener, Context {
private TextField textClock = new TextField(60);
private TextArea textScreen = new TextArea(10, 60);
private Button buttonUse = new Button("금고사용");
private Button buttonAlarm = new Button("비상벨");
private Button buttonPhone = new Button("일반 통화");
private Button buttonExit = new Button("종료");
private State state = DayState.getInstance();
// 생성자
public SafeFrame(String title) {
super(title);
setBackground(Color.lightGray);
setLayout(new BorderLayout());
// textClock레이아웃
add(textClock, BorderLayout.NORTH);
textClock.setEditable(false);
// textScreen 레이아웃
add(textScreen, BorderLayout.CENTER);
textScreen.setEditable(false);
Panel panel = new Panel();
panel.add(buttonUse);
panel.add(buttonAlarm);
panel.add(buttonPhone);
panel.add(buttonExit);
add(panel, BorderLayout.SOUTH);
// 패널 사이즈에 창 크기 맞춤
pack();
show();
// listener 등록
buttonUse.addActionListener(this);
buttonAlarm.addActionListener(this);
buttonPhone.addActionListener(this);
buttonExit.addActionListener(this);
}
// ActionEvent
public void actionPerformed(ActionEvent e) {
System.out.println(e.toString());
if (e.getSource() == buttonUse) { // 금고 사용
state.doUse(this);
} else if (e.getSource() == buttonAlarm) { // 비상벨
state.doAlarm(this);
} else if (e.getSource() == buttonPhone) { // 일반통화
state.doPhone(this);
} else if (e.getSource() == buttonExit) { // 종료
System.exit(0);
} else {
System.out.println("?");
}
}
// 시간 설정
public void setClock(int hour) {
String clockstring = "현재 시간은";
if (hour < 10) {
clockstring += "0" + hour + ":00";
} else {
clockstring += hour + ":00";
}
System.out.println(clockstring);
textClock.setText(clockstring);
state.doClock(this, hour);
}
// 상태 변경
public void changeState(State state) {
System.out.println(this.state + "에서" + state + "로 상태가 변했습니다.");
this.state = state;
}
// 보안센터 전화
public void callSecurityCenter(String msg) {
textScreen.append("call! " + msg + "\n");
}
// 통화 기록
public void recordLog(String msg) {
textScreen.append("record ... " + msg + "\n");
}
}
SafeFrame 클래스
- GUI 사용해서, 금고 경비 시스템 실현
- Safe는 금고라는 뜻
- Context 인터페이스를 구현
- state 필드: 금고의 현재 상태를 저장하는 변수
초기값 => 주간 - 생성자
- 배경색 설정
- 레이아웃 매니저의 설정
- 부품의 배치
- 리스너의 설정
addActionListener 메소드를 이용하여 ActionListener 를 구현한 객체를 버튼에 등록
버튼이 눌려지면 등록된 ActionListener 객체의 actionPerformed() 를 호출
- 버튼의 ActionListener 는 SafeFrame 자신이다.
- actionPerformed(ActionEvent e)
- 눌러진 버튼의 종류에 따라, state.doUse(this) / state.doAlarm(this) / state.doPhone(this) 중 하나를 호출
- 현재 상태가 주간인지 야간인지 체크하는 코드가 필요하다. - setClock()
시간 설정을 위해 클라이언트가 호출하는 메소드 - callSecurityCenter()
경비센터에 대한 호출을 표현함 - recordLog()
경비센터의 로그에 기록하는 일을 표현함
public class Main {
public static void main(String[] args) {
SafeFrame frame = new SafeFrame("State Sample");
while (true) {
for (int hour = 0; hour < 24; hour++) {
frame.setClock(hour); // ����
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
}
Main
- SafeFrame 인스턴스를 한개 만든 후,
- 1초 간격으로 SafeFrame 의 setClock() 메소드를 호출 -> 시간 업데이트
-> Thread.sleep(1000) 문장을 사용함
5.2 등장 역할
- State
- 상태를 나타내는 역할
- 각 상태에 따라 다른 행동을 하는 통일된 인터페이스를 결정함
- State 인터페이스
- ConcreteState(구체적인 상태)의 역할
- 구체적인 개개의 상태를 표현하는 역할
- State 역할이 결정한 인터페이스를 구현
- DayState, NightState
- Context (상황, 전후관계, 문맥)의 역할
- 현재의 상태를 나타내는 ConcreteState 역할을 가지고 있음
- State 패턴 이용자가 필요로 하는 인터페이스를 결정함
- Context, SafeFrame
5.3 힌트
분할해서 통치하기
- divide and conquer
- 복잡하고 규모가 큰 프로그램 => 작은 문제로 나누어서 풀기
- State 패턴
-> 개개의 구체적인 상태를 각각 클래스로 나누어서 표현함으로써 문제를 분할함 - 상태의 종류가 많을수록 유용함
- State 패턴을 사용하지 않는다면, 계속해서 상태를 검사하는 조건문이 필요하다.
- 예: 상태가 10가지 -> if - else if 문이 10개
상태에 의존한 처리
- SafeFrame의 setClock() 과 State 의 doClock() 의 관계
- Main 클래스가 SafeFrame의 setClock() 메소드를 호출해서 시간을 설정하도록 함
- setClock() 메소드 안에서는, state.doClock(this, hour) 를 호출하여 현재 state 에 그 처리를 위임함. '현재의 상태에 의존한 처리'로 취급
- 방법
- 추상메소드로 선언하고 인터페이스로 함
- 구상 메소드로 구현하고 각각의 클래스로 함
- 현재 state 에 따라 행동이 달라지게 됨
새로운 상태를 추가하는 것은 간단
-> 새로운 State 클래스를 만들어서 필요한 메소드를 구현하기만 하면 된다.
STD (State Transition Diagram)
시스템의 상태 변화를 표현하기 위해 사용되는 다이어그램
5.4 연습문제
- Context 인터페이스 -> SafeFrame 클래스에서 이미 Frame 클래스를 상속받아 다른 클래스는 상속받을 수 없음
- 주간, 야간의 범위를 변경하려면
doClock() 안의 if 조건 변경해야함 - 점심 시간 상태 추가
NoonState 클래스 추가
-> 모든 클래스들에서 doClock() 범위 수정
public class NoonState implements State {
private static NoonState singleton = new NoonState();
private NoonState() { // 생성자는 private
}
public static State getInstance() { // 유일한 인스턴스
return singleton;
}
public void doClock(Context context, int hour) { // 시간 설정
if (hour < 9 || 17 <= hour) {
context.changeState(NightState.getInstance());
} else if (9 <= hour && hour < 12 || 13 <= hour && hour < 17) {
context.changeState(DayState.getInstance());
}
}
public void doUse(Context context) { // 금고 사용
context.callSecurityCenter("비상:점심시간 금고사용!");
}
public void doAlarm(Context context) { // 비상벨
context.callSecurityCenter("비상벨(점심시간)");
}
public void doPhone(Context context) { // 일반통화
context.recordLog("점심시간 통화녹음");
}
public String toString() {
return "[점심시간]";
}
}
4. 비상 시 상태 추가
UrgentState 클래스
-> 다른 doAlarm 수정
context.chageState(UrgentState.getInstance()) 추가
-> 비상사태 전환 후, 원래 상태로 복원하는 수단이 없다는 문제점이 있음
public class UrgentState implements State {
private static UrgentState singleton = new UrgentState();
private UrgentState() {
}
public static State getInstance() {
return singleton;
}
public void doClock(Context context, int hour) {
// 아무 처리 하지 않음
}
public void doUse(Context context) { // 금고사용
context.callSecurityCenter("비상:비상사태 금고사용!");
}
public void doAlarm(Context context) { // 비상벨
context.callSecurityCenter("비상벨(비상사태)");
}
public void doPhone(Context context) { // 일반통화
context.callSecurityCenter("일반통화(비상사태)");
}
public String toString() {
return "[비상사태]";
}
}
DayState, NightState 의 doAlarm()
public void doAlarm(Context context) {
context.callSecurityCenter("비상벨(주간)");
context.changeState(UrgentState.getInstance());
}
6. Proxy
- 대리인
- 본인을 대신해서 일을 처리하는 사람
- 퍼포먼스를 위해서 사용함
- proxy가 초기 작업이나 간단한 작업을 처리함
복잡하거나 무거운 요청이 오면 실제 객체가 처리함
6.1 예제 프로그램
이름을 가진 프린터
화면에 문자열을 표시하는 프로그램
Printer | 이름이 붙은 프린터를 나타내는 클래스 |
Printable | Printer 와 PrinterProxy 에서 구현하는 인터페이스 |
PrinterProxy | 이름이 붙은 프린터를 나타내는 클래스 |
Main | 동작 테스트용 클래스 |
Printable 인터페이스
public interface Printable {
public abstract void setPrinterName(String name);
public abstract String getPrinterName();
public abstract void print(String string);
}
- PrinterProxy 와 Printer 클래스를 동일시하기 위한 인터페이스
- Printer 클래스 인스턴스 생성에 많은 시간이 걸림
- heavyJob -> 5초 걸림
Printer 클래스
public class Printer implements Printable {
private String name;
public Printer() {
heavyJob("Printer의 인스턴스를 생성 중");
}
public Printer(String name) {
this.name = name;
heavyJob("Printer의 인스턴스 (" + name + ")을 생성 중");
}
public void setPrinterName(String name) {
this.name = name;
}
public String getPrinterName() {
return name;
}
public void print(String string) {
System.out.println("=== " + name + " ===");
System.out.println(string);
}
private void heavyJob(String msg) {
System.out.print(msg);
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.print(".");
}
System.out.println("완료");
}
}
- '본인'을 나타내는 클래스
- 생성자: heavyJob(오래 걸리는 작업) 호출
- setPrinterName() / getPrinterName()
- print()
proxy 가 할 수 없는 일
프린터 이름과 문자열을 화면에 출력 - heavyJob()
Thread.sleep(1000) 후 '.' 찍는 일을 5번 수행
PrinterProxy
public class PrinterProxy implements Printable {
private String name;
private Printer real;
public PrinterProxy() {
}
public PrinterProxy(String name) {
this.name = name;
}
public synchronized void setPrinterName(String name) {
if (real != null) {
real.setPrinterName(name);
}
this.name = name;
}
public String getPrinterName() {
return name;
}
public void print(String string) {
realize();
real.print(string);
}
private synchronized void realize() {
if (real == null) {
real = new Printer(name);
}
}
}
- Printer 클래스의 대리인을 나타내는 클래스
- name 필드: 대리인의 이름을 저장함
- real 필드: '본인'에 대한 참조를 저장함 (원래 객체 참조)
- setPrintername(): real이 null 이 아니면, 즉, 본인 객체가 이미 생성되어 있으면, 그 객체에도 이름을 설정한다.
- print()
realize() 호출 후, '본인' 객체의 print() 메소드를 호출한다. => 위임 - realize()
실제 일을 하는 '본인'객체가 생성되지 않았으면 생성한다. - Printer는 PrinterProxy의 존재를 모르지만 PrinterProxy는 Printer에 깊이 관여됨
-> 연관관계가 깊어지면 커플링이 높아져서 유지보수가 어려워짐 - setPrintname() 과 realize()는 synchronized로 선언됨
- 멀티스레드 환경에서 동시에 printer name을 변경하거나 real 변수에 새로운 객체를 동시에 대입할 수 있어서
Main 클래스
public class Main {
public static void main(String[] args) {
Printable p = new PrinterProxy("Alice");
System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
p.setPrinterName("Bob");
System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
p.print("Hello, world.");
}
}
// 실행 결과
이름은 현재 Alice입니다.
이름은 현재 Bob입니다.
Printer의 인스턴스 (Bob)을 생성 중.....완료
=== Bob ===
Hello, world.
- PrinterProxy를 경유해서, Printer를 이용하는 클래스
- 먼저, PrinterProxy 생성 후, 이름을 설정, "hello, world" 출력
- 이름을 설정하거나 표시되는 동안에는, Printer 클래스의 인스턴스는 생성되지 않음
- print() 메소드가 호출 될 때, Printer 객체가 생성된다.
-> 필요할 때 생성됨
-> Printer 객체가 필요한 시점에 생성해서 printer 에게 일을 위임
6.2 등장 역할
- Subject (주체) 역할
- Proxy와 RealSubject를 동일시하기 위한 인터페이스를 정의
- Printable 인터페이스가 해당
- Proxy(대리인)의 역할
- Client로부터 요구를 가능한 한 자신이 처리함
- 혼자서 처리할 수 없을 때, RealSubject에게 위임
- Proxy 역할은, RealSubject 역할이 정말로 필요해지면 그때 RealSubject역할을 생성
- PrinterProxy
- Client (의뢰자)
- Main
- Proxy 패턴을 이용하는 역할
6.3 힌트
- 대리인을 사용해서 스피드업
- Proxy 역할이 대리인이 됨, 할 수 있는 일은 모두 처리
- 실제로 무거운 처리는 필요할 때까지 지연시킴 (인스턴스 생성)
- 초기화하는데 시간이 많이 걸리는 대규모 시스템에서 유용함
- 기동 시점에서 이용하지 않는 기능까지 전부 초기화하면, 애플리케이션 기동에 시간이 많이 걸림
- 시간이 걸리는 아직 필요하지 않은 초기화 -> 필요한 시점까지 미루기
- GOF의 예
GUI 객체들을 포함한 문서 에디터
문서를 열때는 그래픽 객체들 생성하지 않고, 화면에 표시할 때가 되면 생성 - Proxy 와 RealSubject의 분리
- RealSubject 안에 지연평가 기능을 넣을수도 있다.
- 분리하는 것이 수정 및 관리에 용이함 (유지보수가 용이)
- 대리와 위임
- 대리인이 처리할 수 있는 일은 대리인이 처리하고 (Proxy)
대리인이 처리할 수 없을 때 본인에게 위임한다(RealSubject)
- 대리인이 처리할 수 있는 일은 대리인이 처리하고 (Proxy)
- Transparent
- 클라이언트 입장에서는, 실제로 호출하는 것이 PrinterProxy 인지, Printer 객체인지 상관 없음
(투명하게 보임 - 안보임)
- 클라이언트 입장에서는, 실제로 호출하는 것이 PrinterProxy 인지, Printer 객체인지 상관 없음
- HTTP Proxy
- Proxy는 HTTP 서버와 HTTP 클라이언트 사이에 들어가, 웹 페이지의 캐싱 등을 실행하는 소프트웨어
- 프록시 패턴 적용
- 웹 브라우저가 웹 페이지를 표시할 때, 일일이 원격지에 있는 웹 서버로부터 페이지를 얻어오는 것이 아님
- 대신, HTTP 프록시가 캐쉬한 페이지를 얻어온다.
- 최신 정보가 필요하게 되었거나, 페이지의 유효기간이 다 되었을 때에 비로소 웹서버로 웹 페이지를 가지러 감
- 웹 브라우저 => Client 역할
- HTTP Proxy -> Proxy 역할
- 웹서버 -> RealSubject 역할
public class PrinterProxy implements Printable {
private String name;
private Printable real;
private String className;
public PrinterProxy(String name, String className) {
this.name = name;
this.className = className;
}
public synchronized void setPrinterName(String name) {
if (real != null) {
real.setPrinterName(name);
}
this.name = name;
}
public String getPrinterName() {
return name;
}
public void print(String string) {
realize();
real.print(string);
}
private synchronized void realize() {
if (real == null) {
try {
real = (Printable)Class.forName(className).newInstance();
real.setPrinterName(name);
} catch (ClassNotFoundException e) {
System.err.println("클래스 " + className + " 가 발견되지 않습니다.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- Printable 멤버변수, String className
- Class.forName(className).newInstance();
className 이름으로 된 클래스의 인스턴스를 생성 - 현재
real = (Printable)Class.forName(className).getDeclaredConstructor().newInstance();
이렇게 변경됨
멀티스레드 상황에서 동기화되어있지 않으면 데이터의 신뢰성과 안정성에 문제가 발생할 수 있음
Printer - name
PrinterProxy - name 값이 달라질 수 있음
7. Command
- 클래스(객체)가 일을 처리할 때는, 자신의 클래스나 다른 클래스의 메소드를 호출한다.
- Command패턴에서는, 실행하고자 하는 일이 메소드 호출이 아닌,
'명령을 나타내는 클래스'의 인스턴스 생성으로 표현 - 이점
- history를 관리하고 싶을 때, Command클래스의 인스턴스의 집합을 관리하면 된다.
- 명령의 집합을 보존해두면, 똑같은 명령을 재실행 할 수도 있고, 또는 여러개의 명령을 모아서 새로운 명령으로서 재사용할수도 있다.
7.1 예제 프로그램
- 간단한 그림 그리기 프로그램
- 마우스를 드래그하면 빨간 점이 연결되어 그림이 그려진다.
- 'clear' 버튼을 누르면 점이 모두 지워진다.
- 사용자가 마우스를 끌 때마다, "이 위치에 점을 그려라" 라는 명령이 DrawCommand 클래스의 인스턴스로 생성된다.
패키지 | 이름 | 해설 |
command | Command | '명령'을 표현하는 인터페이스 |
command | MacroCommand | '여러 개의 명령을 모은 명령'을 나타내는 클래스 |
drawer | DrawCommand | '점 그리기 명령'을 표현한 클래스 |
drawer | Drawable | '그리기 대상'을 표현한 인터페이스 |
drawer | DrawCanvas | '그리기 대상'을 구현한 클래스 |
Anonymous | Main | 동작 테스트용 클래스 |
Command 인터페이스
- '명령'을 표현하기 위한 인터페이스
- execute()
- 무언가를 실행하는 메소드
- 구체적으로 무슨 일을 하는지는 Command 인터페이스를 구현한 클래스가 결정
- MacroCommand와 DrawCommand가 이 인터페이스 구현
MacroCommand
- '여러개의 명령을 한데 모은 명령'을 나타냄
- Composite 패턴이 사용됨
- 여러 개의 명령을 모은 것이면서, 그 자체가 하나의 명령이 된다.
- commands 필드
- java.util.Stack 타입
-> Undo 구현을 위해 Stack 사용 - 실행한 command 집합
- java.util.Stack 타입
- execute()
- 자신이 가지고 있는 모든 명령의 execute를 실행
- MacroCommand가 가지고 있는 명령들의 execute() 가 차례대로 실행됨
(recursive call, Composite 패턴)
- append()
- MacroCommand 클래스에 새로운 Command(Command 인터페이스를 구현한 클래스의 인스턴스)를 추가하는 메소드
- Stack 클래스의 push() 를 이용
- 실수로 자기 자신을 추가하지 않도록 체크함
-> 자기 자신이 추가되면, execute() 실행 시 무한루프가 돌게 된다.
- undo()
- commands 의 마지막 명령을 삭제하는 메소드
- Stack 클래스의 pop() 을 이용함
- clear()
- commands 의 모든 명령을 삭제하는 메소드
DrawCommand 클래스
- Command 인터페이스 구현
- '그림 그리기 명령'을 표현함
- drawable 필드: 그림 그리기를 실행할 대상을 저장함
- position 필드: 그림 그리기를 행할 위치를 나타냄
java.awt.Point 클래스: X좌표와 Y좌표를 갖는 클래스
-> 2차원 평면 상의 위치를 나타냄 - 생성자의 매개변수
- drawable
어떤 개체를 대상으로 하는 그리기 명령인가를 나타내기 위한 인자 - position
어디에 그리는 명령인가를 나타내기 위한 인자 - '이 위치에 점을 그려라'라는 명령을 생성하는 생성자
- drawable
- execute()
- drawable 필드의 draw() 메소드 호출
- 실제 그리기를 실행하는 메소드
package Sample.command;
public interface Command {
public abstract void execute();
}
package Sample.command;
import java.util.Stack;
import java.util.Iterator;
public class MacroCommand implements Command {
private Stack commands = new Stack();
public void execute() {
Iterator it = commands.iterator();
while (it.hasNext()) {
((Command)it.next()).execute();
}
}
public void append(Command cmd) {
if (cmd != this) {
commands.push(cmd);
}
}
public void undo() {
if (!commands.empty()) {
commands.pop();
}
}
public void clear() {
commands.clear();
}
}
package Sample.drawer;
import Sample.command.Command;
import java.awt.Point;
public class DrawCommand implements Command {
protected Drawable drawable;
private Point position;
public DrawCommand(Drawable drawable, Point position) {
this.drawable = drawable;
this.position = position;
}
public void execute() {
drawable.draw(position.x, position.y);
}
}
Drawable 인터페이스
- '그림 그리기 대상'을 표현함
- draw(int x, int y) 메소드를 가짐
- x, y: 그림 그릴 때의 좌표를 나타냄
DrawCanvas 클래스
- implements Drawable 인터페이스
extends java.awt.Canvas 클래스 상속 - history
지금까지 실행한 명령어 집합 가짐 - 생성자
폭, 높이, 그림 그리기 history를 받아서, DrawCanvas 인스턴스를 초기화한다. - paint()
- DrawCanvas를 다시 그릴 필요가 생겼을 때, ,java.awt 프레임워크로부터 자동 호출됨
- repaint() 메소드가 호출되면, 자동으로 paint() 메소드가 실행
- history가 보관하고 있는 모든 그리기 명령들을 실행
commands 필드 -> 루프를 돌면서 앞에 명령부터 실행
- draw()
- Graphics 객체를 얻어서 색을 빨간색으로 지정,
- Graphics 객체의 fillOval(x, y, 사각형 가로, 사각형 세로 길이)를 호출해 원을 그린다
package Sample.drawer;
public interface Drawable {
public abstract void draw(int x, int y);
}
package Sample.drawer;
import Sample.command.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class DrawCanvas extends Canvas implements Drawable {
private Color color = Color.red;
private int radius = 6;
private MacroCommand history;
public DrawCanvas(int width, int height, MacroCommand history) {
setSize(width, height);
setBackground(Color.white);
this.history = history;
}
public void paint(Graphics g) {
history.execute();
}
public void draw(int x, int y) {
Graphics g = getGraphics();
g.setColor(color);
g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
}
}
Main 클래스
- 예제 프로그램을 작동시키는 클래스
- JFrame 상속, ActionListener, MousesMotionListener, WindowListener 구현
- history 필드
DrawCanvas 생성 시 인자로 넘겨줌
(Main 인스턴스와 DrawCanvas 인스턴스가 history를 공유) - canvas 필드
그림 그리는 영역을 나타냄 - clearButton 필드
javax.swing.JButton 클래스
그린 점 모두 지움 - actionPerformed()
- clearButton 이 눌러졌을 때 호출
ActionEvent 발생 시 호출되는 이벤트 핸들러 - history에 보관되어있던 모든 명령을 지우고, 캔버스의 repaint() 가 호출된다.
현재 history에 아무 명령도 들어있지 않으므로, 캔버스에 아무 내용도 그려지지 않음
- clearButton 이 눌러졌을 때 호출
- mouseDragged()
- 사용자가 마우스를 drag 하면 이 메소드가 호출된다.
- 그리기 명령을 나타내는 DrawCommand 객체를 생성한 후, history에 추가
- cmd.execute() 를 호출하여 지정 위치에 빨간 점을 그린다. (fillOval())
- 사용자가 마우스를 drag 하면 이 메소드가 호출된다.
- windowClosing()
- 창의 오른쪽 아이콘 중에 X 를 눌렀을 때 호출되는 메소드
- System.exit() (현재 exitOnClose() 사용)
을 사용해서 프로그램을 종료한다.
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
- 생성자
- 버튼 클릭, 마우스 클릭 등의 이벤트를 받아들이는 리스너를 설정함
- 여러가지 GUI 부품을 배치함
- Box 객체 이용
- Box
가벼운 컨테이너, 일종의 패널
(컨테이너는 레이아웃매니저를 가짐.(컴포넌트 배치 방법 정의))
- BoxLayout.X_AXIS
(수평) 가로로 배치 시 사용 [ 0 0 0 ] - BoxLayout.Y_AXIS
(수직) 세로 배치 시 사용
(프레임 사이즈가 바뀌어도 항상 수직으로 유지됨)
0
0
0 - BoxLayout을 쓰는 컨테이너
- GridBagLayout 보다 쉽게 GridBagLayout과 비슷하도록 만들 수 있다.
- BoxLayout.X_AXIS
- Panel
FlowLayout 을 쓰는 컨테이너
package Sample;
import Sample.command.*;
import Sample.drawer.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {
private MacroCommand history = new MacroCommand();
private DrawCanvas canvas = new DrawCanvas(400, 400, history);
private JButton clearButton = new JButton("clear");
public Main(String title) {
super(title);
this.addWindowListener(this);
canvas.addMouseMotionListener(this);
clearButton.addActionListener(this);
Box buttonBox = new Box(BoxLayout.X_AXIS);
buttonBox.add(clearButton);
Box mainBox = new Box(BoxLayout.Y_AXIS);
mainBox.add(buttonBox);
mainBox.add(canvas);
getContentPane().add(mainBox);
pack();
show();
}
// ActionListener
public void actionPerformed(ActionEvent e) {
if (e.getSource() == clearButton) {
history.clear();
canvas.repaint();
}
}
// MouseMotionListener
public void mouseMoved(MouseEvent e) {
}
public void mouseDragged(MouseEvent e) {
Command cmd = new DrawCommand(canvas, e.getPoint());
history.append(cmd);
cmd.execute();
}
// WindowListener
public void windowClosing(WindowEvent e) {
System.exit(0);
}
public void windowActivated(WindowEvent e) {}
public void windowClosed(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
public static void main(String[] args) {
new Main("Command Pattern Sample");
}
}
- 예제 프로그램 행동 방식
- 마우스가 드래그 될 때마다, 어느 좌표에 빨간 점이 그려졌다는 명령을 나타내는 DrawCommand 명령어가 history에 추가된다.
- 이력 유지, redo, undo 사용 가능
- clearButton 이 눌려지면, history안의 모든 DrawCommand객체들이 사라짐
- 마우스가 드래그 될 때마다, 어느 좌표에 빨간 점이 그려졌다는 명령을 나타내는 DrawCommand 명령어가 history에 추가된다.
7.2 등장 역할
- Command 역할
- 명령의 인터페이스를 정의하는 역할
- Command 인터페이스
- ConcreteCommand 의 역할
- Command 인터페이스를 구현
- MacroCommand, DrawCommand
- Receiver의 역할
- Command 명령을 실행할 때 대상이 되는 역할
- 명령을 받아들이는 객체
- DrawCanvas
- Client의 역할
- ConcreteCommand 를 생성하고, Receiver를 할당하는 역할
- Main이 해당됨
- Invoker의 역할
- 명령을 처음 실행
- Main, DrawCanvas
history.execute() 호출
7.3 힌트
- 명령이 지니고 있어야 하는 정보
- 예제:
DrawCommand는 그리기 대상과 점의 위치에 대한 정보만 가짐 - 필요에 따라 점의 크기, 색, 모양, 이벤트 발생 시각등에 대한 정보 추가 가능
- 예제:
- 이력의 보존
- history멤버 변수는 MacroCommand 타입
지금까지 발생된 모든 그리기 명령(정보)를 가지고 있다. - 이 멤버 변수를 파일로 보존하면 '그리기 이력'이 보존된다.
- history멤버 변수는 MacroCommand 타입
- 어댑터
- Main 클래스가 구현하는 인터페이스 중
WindowListener 인터페이스는 여러 메소드가 선언되어 있어 모두 구현해야 함
windowClosing(), windowActivated(), windowClosed(), windowDeactivated(), windowDeiconified(), windowOpened() - Adapter는 인터페이스의 메소드들을 모두 빈칸으로 구현해두었기 때문에 필요한 것만 구현해주면 된다.
- But, adapter는 클래스이므로 다중 상속이 불가능하다.
-> 익명 클래스 사용 - 인터페이스에 사용하지 않는 메소드들까지 구현해야하는 문제르 해결하기 위해 어댑터 클래스들이 java.awt.event 패키지에 제공됨
- WindowAdapter를 상속받고 원하는 메소드만 재정의하면 됨
- Anonymous inner class
new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
}}- 내부적으로 MouseMotionAdapter 를 상속받고 mouseDragged() 메소드를 재정의한 자식 클래스가 만들어진 후 그 클래스의 객체가 생성됨
- 익명의 이너 클래스를 이용하면 더 깔끔하게 사용 가능
- 사용과 정의를 동시에 할 수 있다.
한번만 사용될 클래스를 새로 정의하지 않고 바로 짜준 것
- Main 클래스가 구현하는 인터페이스 중
canvas.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
Command cmd = new DrawCommand(canvas, e.getPoint());
history.append(cmd);
cmd.execute();
}
});
- 익명의 이너 클래스를 컴파일 한 경우
Main$1.class 파일이 생성된다.
* '명령'을 객체로 표현해서 이력을 보관하기도 하고, 재실행을 할 수도 있는 Command 패턴 *
7.4 예제
1) ColorCommand 클래스
public class ColorCommand implements Command {
protected Drawable drawable;
private Color color;
// 생성자
public ColorCommand(Drawable drawable, Color color) {
this.drawable = drawable;
this.color = color;
}
public void execute() {
drawable.setColor(color);
}
}
- drawable.setColor(color)
붓의 색을 변경한다.
public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {
private MacroCommand history = new MacroCommand();
private DrawCanvas canvas = new DrawCanvas(400, 400, history);
private JButton clearButton = new JButton("clear");
private JButton redButton = new JButton("red");
private JButton greenButton = new JButton("green");
private JButton blueButton = new JButton("blue");
//생성자
public Main(String title) {
super(title);
this.addWindowListener(this);
canvas.addMouseMotionListener(this);
clearButton.addActionListener(this);
redButton.addActionListener(this);
greenButton.addActionListener(this);
blueButton.addActionListener(this);
Box buttonBox = new Box(BoxLayout.X_AXIS);
buttonBox.add(clearButton);
buttonBox.add(redButton);
buttonBox.add(greenButton);
buttonBox.add(blueButton);
Box mainBox = new Box(BoxLayout.Y_AXIS);
mainBox.add(buttonBox);
mainBox.add(canvas);
getContentPane().add(mainBox);
pack();
show();
}
// ActionListener
public void actionPerformed(ActionEvent e) {
if (e.getSource() == clearButton) {
history.clear();
canvas.init();
canvas.repaint();
} else if (e.getSource() == redButton) {
Command cmd = new ColorCommand(canvas, Color.red);
history.append(cmd); // 색깔 버튼이 눌려지면 ColorCommand 객체를 command에 추가
cmd.execute();
} else if (e.getSource() == greenButton) {
Command cmd = new ColorCommand(canvas, Color.green);
history.append(cmd);
cmd.execute();
} else if (e.getSource() == blueButton) {
Command cmd = new ColorCommand(canvas, Color.blue);
history.append(cmd);
cmd.execute();
}
}
2) 마지막으로 그린 점을 삭제
- history의 undo 기능 사용 (가장 마지막 항목을 삭제한 뒤 repaint())
Main에 추가된 부분
public void actionPerformed(ActionEvent e) {
if (e.getSource() == clearButton) {
history.clear();
canvas.repaint();
} else if (e.getSource() == undoButton) {
history.undo();
canvas.repaint();
}
}
MacroCommand에 추가된 부분
public void undo() {
if (!commands.empty()) {
commands.pop();
}
}
3) Listener 인터페이스 대신 어댑터로 변경하기
Main생성자 코드 수정
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
canvas.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
Command cmd = new DrawCommand(canvas, e.getPoint());
history.append(cmd);
cmd.execute();
}
});