LostCatBox

(ETC) 디자인 패턴에 대해서 CH3 (행동 디자인 패턴에 대해)

Word count: 3.3kReading time: 20 min
2023/04/22 Share

행동 패턴 참조

전체 참조

메멘토 패턴

중재자 패턴

반복자 패턴

커맨드 패턴

책임 연쇄 패턴

책임 연쇄 패턴

사용 상황 요약

책임 연쇄 패턴은 핸들러들의 체인(사슬)을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴입니다. 각 핸들러는 요청을 받으면 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지를 결정합니다.

책임 연쇄 패턴이란?

책임 연쇄 패턴은 특정 행동들을 핸들러라는 독립 실행형 객체들로 변환합니다. 당신의 앱의 경우 각 검사는 검사를 수행하는 단일 메서드가 있는 자체 클래스로 추출되어야 합니다. 이제 요청은 데이터와 함께 이 메서드에 인수로 전달됩니다.

스크린샷 2023-04-22 오후 4.18.01

  1. 핸들러는 모든 구상 핸들러에 공통적인 인터페이스를 선언합니다. 일반적으로 여기에는 요청을 처리하기 위한 단일 메서드만 포함되지만 때로는 체인의 다음 핸들러를 세팅하기 위한 다른 메서드가 있을 수도 있습니다.

  2. 기초 핸들러는 선택적 클래스이며 여기에 모든 핸들러 클래스들에 공통적인 상용구 코드를 넣을 수 있습니다.

    일반적으로 이 클래스는 다음 핸들러에 대한 참조를 저장하기 위한 필드를 정의합니다. 클라이언트들은 핸들러를 이전 핸들러의 생성자 또는 세터(setter)에 해당 핸들러를 전달하여 체인을 구축할 수 있습니다. 또 클래스는 디폴트 핸들러 행동을 구현할 수도 있습니다. 즉, 다음 핸들러의 존재 여부를 확인한 후 다음 핸들러로 실행을 넘길 수 있습니다.

  3. 구상 핸들러들에는 요청을 처리하기 위한 실제 코드가 포함되어 있습니다. 각 핸들러는 요청을 받으면 이 요청을 처리할지와 함께 체인을 따라 전달할지를 결정해야 합니다.

    핸들러들은 일반적으로 자체 포함형이고 불변하며, 생성자를 통해 필요한 모든 데이터를 한 번만 받습니다.

  4. 클라이언트는 앱의 논리에 따라 체인들을 한 번만 구성하거나 동적으로 구성할 수 있습니다. 참고로 요청은 체인의 모든 핸들러에 보낼 수 있으며, 꼭 첫 번째 핸들러일 필요는 없습니다.

예시

좋은예시 filterChain

Chain 인터페이스

1
2
3
4
5
public interface Chain {
public abstract void setNext(Chain nextChain);

public abstract void process(Number request);
}

Number 클래스

1
2
3
4
5
6
7
8
9
10
public class Number {
private int number;

public Number(int number) {
this.number = number;
}
public int getNumber(){
return this.number;
}
}

Process 1,2,3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NegativeProcess implements Chain{
private Chain nextChain;
@Override
public void setNext(Chain nextChain) {
this.nextChain = nextChain;
}

@Override
public void process(Number request) {
if (request.getNumber() < 0) {
System.out.println("Negative");
}
else {
nextChain.process(request);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ZeroProcess implements Chain{
private Chain nextChain;
@Override
public void setNext(Chain nextChain) {
this.nextChain = nextChain;
}

@Override
public void process(Number request) {
if (request.getNumber() == 0) {
System.out.println("Zero");
}
else {
nextChain.process(request);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PositiveProcess implements Chain{
private Chain nextChain;
@Override
public void setNext(Chain nextChain) {
this.nextChain = nextChain;
}

@Override
public void process(Number request) {
if (request.getNumber() > 0) {
System.out.println("Positive");
}
else {
nextChain.process(request);
}
}
}
  • 실행
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Chain c1 = new NegativeProcess();
Chain c2 = new ZeroProcess();
Chain c3 = new PositiveProcess();

c1.setNext(c2);
c2.setNext(c3);

c1.process(new Number(1111));
}

장단점

장점

  • 요청의 처리 순서를 제어할 수 있습니다.
  • 단일 책임 원칙. 당신은 작업을 호출하는 클래스들을 작업을 수행하는 클래스들과 분리할 수 있습니다.
  • 개방/폐쇄 원칙. 기존 클라이언트 코드를 손상하지 않고 앱에 새 핸들러들을 도입할 수 있습니다.

단점

  • 일부 요청들은 처리되지 않을 수 있습니다.

커맨드 패턴

사용 상황 요약

커맨드는 요청을 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴입니다. 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 취소할 수 있는 작업을 지원할 수 있도록 합니다.

즉, 호출자와 수신자를 분리한다. -> Button을 호출했지만, 어떤 기능이든 실행가능한 버튼 생성가능(호출자), 실제로 로직수행하는 객체(수신자)

커맨드 패턴이란?

여러 기능을 구현한 버튼을 만들때, 자식들을 여러개 만드는 것이 아니라, 버튼 객체에 execute() 메서드를 가진 인터페이스를 의존하게한다. 이제 기능에 따라 execute()를 구현한 구현체만 골라서 실행하면된다.

스크린샷 2023-04-22 오후 4.57.42

  1. 발송자 클래스(invoker라고도 함)는 요청들을 시작하는 역할을 합니다. 이 클래스에는 커맨드 객체에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 발송자는 요청을 수신자에게 직접 보내는 대신 해당 커맨드를 작동시킵니다. 참고로 발송자는 커맨드 객체를 생성할 책임이 없으며 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 커맨드를 받습니다.

  2. 커맨드 인터페이스는 일반적으로 커맨드를 실행하기 위한 단일 메서드만을 선언합니다.

  3. 구상 커맨드들은 다양한 유형의 요청을 구현합니다. 구상 커맨드는 자체적으로 작업을 수행해서는 안 되며, 대신 비즈니스 논리 객체 중 하나에 호출을 전달해야 합니다. 그러나 코드를 단순화하기 위해 이러한 클래스들은 병합될 수 있습니다.

    수신 객체에서 메서드를 실행하는 데 필요한 매개 변수들은 구상 커맨드의 필드들로 선언할 수 있습니다. 생성자를 통해서만 이러한 필드들의 초기화를 허용함으로써 커맨드 객체들을 불변으로 만들 수 있습니다.

  4. 수신자 클래스에는 일부 비즈니스 로직이 포함되어 있습니다. 거의 모든 객체는 수신자 역할을 할 수 있습니다. 대부분의 커맨드들은 요청이 수신자에게 전달되는 방법에 대한 세부 정보만 처리하는 반면 수신자 자체는 실제 작업을 수행합니다.

  5. 클라이언트는 구상 커맨드 객체들을 만들고 설정합니다. 클라이언트는 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드의 생성자로 전달해야 하며 그렇게 만들어진 커맨드는 하나 또는 여러 발송자와 연관될 수 있습니다.

예시

  • Command
1
2
3
public interface Alarm {
void execute();
}
  • Lamp, Alarm

1
2
3
4
5
6
7
8
public class Alarm {
public void start(){ System.out.println("Alarming"); }
}


public class Lamp {
public void turnOn(){ System.out.println("Lamp On"); }
}
  • AlarmStartCommand, LampOnCommand
1
2
3
4
5
6
7
8
9
10
11
12
13
public class AlarmStartCommand implements Command {
private Alarm theAlarm;
public AlarmStartCommand(Alarm theAlarm) { this.theAlarm = theAlarm; }
// Command 인터페이스의 execute 메서드
public void execute() { theAlarm.start(); }
}

public class LampOnCommand implements Command {
private Lamp theLamp;
public LampOnCommand(Lamp theLamp) { this.theLamp = theLamp; }
// Command 인터페이스의 execute 메서드
public void execute() { theLamp.turnOn(); }
}
  • Button
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Button {
private Command command;

public Button(Command command){
this.command = command;
}
public void setCommand(Command newCommend){
this.command = newCommend;
}
public void pressed(){
this.command.execute();
}
}
  • 사용코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Pattern2Main {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Command lampOnCommand = new LampOnCommand(lamp);
Alarm alarm = new Alarm();
Command alarmStartCommand = new AlarmStartCommand(alarm);

Button button1 = new Button(lampOnCommand); // 램프 켜는 Command 설정
button1.pressed(); // 램프 켜는 기능 수행

Button button2 = new Button(alarmStartCommand); // 알람 울리는 Command 설정
button2.pressed(); // 알람 울리는 기능 수행
button2.setCommand(lampOnCommand); // 다시 램프 켜는 Command로 설정
button2.pressed(); // 램프 켜는 기능 수행
}
}

장단점

장점

  • 단일 책임 원칙. 작업을 호출하는 클래스들을 이러한 작업을 수행하는 클래스들로부터 분리할 수 있습니다.
  • 개방/폐쇄 원칙. 기존 클라이언트 코드를 손상하지 않고 앱에 새 커맨드들을 도입할 수 있습니다.
  • 실행 취소/다시 실행을 구현할 수 있습니다.
  • 작업들의 지연된 실행을 구현할 수 있습니다.
  • 간단한 커맨드들의 집합을 복잡한 커맨드로 조합할 수 있습니다.

단점

  • 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 때문에 코드가 더 복잡해질 수 있습니다.

반복자(중요X)

사용 상황 요약

  • 반복자는 컬렉션의 요소들의 기본 표현(리스트, 스택, 트리 등)을 노출하지 않고 그들을 하나씩 순회할 수 있도록 하는 행동 디자인 패턴입니다.
  • 접근기능과 자료구조를 분리시켜서 객체화합니다. 서로 다른 구조를 가지고 있는 저장 객체에 대해서 접근하기 위해서 interface를 통일시키고 싶을 때 사용하는 패턴입니다.

반복자 패턴 이란?

스크린샷 2023-04-22 오후 5.15.33

  1. 반복자 인터페이스는 컬렉션의 순회에 필요한 작업들(예: 다음 요소 가져오기, 현재 위치 가져오기, 반복자 다시 시작 등)을 선언합니다.

  2. 구상 반복자들은 컬렉션 순회를 위한 특정 알고리즘들을 구현합니다. 반복자 객체는 순회의 진행 상황을 자체적으로 추적해야 합니다. 이는 여러 반복자들이 같은 컬렉션을 서로 독립적으로 순회할 수 있도록 합니다.

  3. 컬렉션 인터페이스는 컬렉션과 호환되는 반복자들을 가져오기 위한 하나 이상의 메서드들을 선언합니다. 참고로 메서드들의 반환 유형은 반복자 인터페이스의 유형으로 선언되어야 합니다. 그래야 구상 컬렉션들이 다양한 유형의 반복자들을 반환할 수 있기 때문입니다.

  4. 구상 컬렉션들은 클라이언트가 요청할 때마다 특정 구상 반복자 클래스의 새 인스턴스들을 반환합니다. 당신은 컬렉션의 나머지 코드가 어디에 있는지 궁금하실 수도 있습니다. 걱정하지 마세요, 같은 클래스에 있을 것입니다. 나머지 코드가 어디에 있는지와 같은 세부 사항들은 실제 패턴에 중요하지 않으므로 생략하기로 했습니다.

  5. 클라이언트는 반복자들과 컬렉션들의 인터페이스를 통해 그들과 함께 작동합니다. 이렇게 하면 클라이언트가 구상 클래스들에 결합하지 않으므로 같은 클라이언트 코드로 다양한 컬렉션들과 반복자들을 사용할 수 있도록 합니다.

    일반적으로 클라이언트들은 자체적으로 반복자들을 생성하지 않고 대신 컬렉션들에서 가져옵니다. 그러나 어떤 경우에는 (예를 들어 클라이언트가 자체 특수 반복자를 정의할 때) 클라이언트가 반복자를 직접 만들 수 있습니다.

예시

BookShelf 예제

책꽂이 안에 책을 꽂고, 다시 책을 하나씩 확인하는 예제를 작성한다.

1) Book : 한권의 책에 대한 정보를 가지고 있는 클래스

1
2
3
4
5
6
7
8
9
10
11
public class Book {
private String name;

public Book(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

2) Aggregate : 집합체를 의미하는 인터페이스

Aggregate는 Iterator 역할을 만들어내는 인터페이스를 결정한다.

1
2
3
4
public interface Aggregate {

public abstract Iterator createIterator();
}

3) BookShelf : 책을 보관하는 책꽂이 역할을 하는 클래스

BookShelf는 이러한 Aggregate의 구현체이다.
실제로 책꽂이 안을 돌아다닐 Iterator를 생성하고 책꽂이를 관리하는 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BookShelf implements Aggregate {
private Book[] books; // 책의 집합
private int last = 0; // 마지막 책이 꽂힌 위치

public BookShelf(int size) {
books = new Book[size];
}

public Book getBook(int index) {
return books[index];
}

public int getLength() {
return last;
}

// 책꽂이에 책을 꽂는다
public void appendBook(Book book) {
if (last < books.length) {
this.books[last] = book;
last++;
} else {
System.out.println("책꽂이가 꽉 찼습니다!");
}
}

@Override
public Iterator createIterator() {
return new BookShelfIterator(this);
}
}

4) BookShelfIterator : BookShelf 클래스에서 검색을 수행하는 클래스

BookShelfIterator를 Iterator로 다루기 위해 Iterator 인터페이스를 상속받았다.

Iterator 인터페이스는 Java에 이미 내장되어 있기 때문에 따로 구현하지 않았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BookShelfIterator implements Iterator<Book> {
private BookShelf bookShelf; // 검색을 수행할 책꽂이
private int index = 0; // 현재 처리할 책의 위치

public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
}

@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}

@Override
public Book next() {
Book book = bookShelf.getBook(index);
index++;
return book;
}
}

next()는 다음번 요소를 반환하고, hasNext()는 검색을 계속 수행해도 될지의 여부를 판별한다.

Main 클래스에서 책꽂이에 책을 꽂고 책을 하나씩 검색해 이름을 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {

public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(10);

Book book1 = new Book("Bilbe");
Book book2 = new Book("Cinderella");
Book book3 = new Book("Daddy-Long-Legs");

bookShelf.appendBook(book1);
bookShelf.appendBook(book2);
bookShelf.appendBook(book3);

System.out.println("현재 꽂혀있는 책 : " + bookShelf.getLength() + "권");

Iterator it = bookShelf.createIterator();
while (it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}
}
}

BookShelfIterator는 책꽂이에서 책을 한권씩 뽑아오는 역할을 한다.

검색할 책이 존재하는 동안 while문이 수행되며, it.next()에 의해 한권씩 책을 꺼내 이름을 출력한다.

Iterator 패턴을 적용하면 어떤 장점이 있을까?

for문으로 동일한 표현이 가능한데 왜 굳이 코드를 늘려가며 Iterator 패턴을 사용해야 하는지 의문이 들 수 있다.

Iterator 패턴을 사용하는 가장 큰 이유는 하나씩 꺼내서 처리하는 과정을 구현과 분리할 수 있기 때문이다.

다음의 코드를 살펴보자.

1) Iterator 패턴을 사용했을 때

1
2
3
4
while (it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}

2) for문을 사용했을 때

1
2
3
for (int i = 0; i < bookShelf.getLength(); i++) {
System.out.println(bookShelf.getBook(i).getName());
}

이 두가지 코드는 실행 시 Book 객체의 내용을 하나씩 꺼내서 보여주는 동일한 결과를 보여준다.

하지만 for문을 사용할 때와 달리 Iterator 패턴을 사용할 때는 어디까지나 Iterator의 메서드를 사용할 뿐 BookShelf의 구현에서 사용되고 있는 메서드는 호출되지 않고있다.

즉, 1번 방식은 BookShelf의 구현에 의존하지 않는다.

BookShelf에 무언가 수정사항이 생기더라도 BookShelf가 올바른 Iterator를 반환하기만 한다면 while문이 사용되는 Main은 수정할 필요가 없다. 하나의 클래스를 수정하더라도 다른 클래스에 큰 영향 없이 작은 수정 만으로도 끝낼 수 있다는 것이다.

장단점

장점

  • 단일 책임 원칙. 부피가 큰 순회 알고리즘들을 별도의 클래스들로 추출하여 클라이언트 코드와 컬렉션들을 정돈할 수 있습니다.
  • 개방/폐쇄 원칙. 새로운 유형의 컬렉션들과 반복자들을 구현할 수 있으며 이들을 아무것도 훼손하지 않은 체 기존의 코드에 전달할 수 있습니다.
  • 당신은 이제 같은 컬렉션을 병렬로 순회할 수 있습니다. 왜냐하면 각 반복자 객체에는 자신의 고유한 순회 상태가 포함되어 있기 때문입니다.
  • 같은 이유로 당신은 순회를 지연하고 필요할 때 계속할 수 있습니다.

단점

  • 당신의 앱이 단순한 컬렉션들과만 작동하는 경우 반복자 패턴을 적용하는 것은 과도할 수 있습니다.
  • 반복자를 사용하는 것은 일부 특수 컬렉션들의 요소들을 직접 탐색하는 것보다 덜 효율적일 수 있습니다

중재자 패턴

사용 상황 요약

  • 중재자는 객체 간의 혼란스러운 의존 관계들을 줄일 수 있는 행동 디자인 패턴입니다. 이 패턴은 객체 간의 직접 통신을 제한하고 중재자 객체를 통해서만 협력하도록 합니다.

중재자 패턴이란?

스크린샷 2023-04-22 오후 5.24.20

  1. 컴포넌트들은 어떤 비즈니스 로직을 포함한 다양한 클래스들입니다. 각 컴포넌트에는 중재자에 대한 참조가 있는데, 이 중재자는 중재자 인터페이스의 유형으로 선언됩니다. 컴포넌트는 중재자의 실제 클래스를 인식하지 못하므로 컴포넌트를 다른 중재자에 연결하여 다른 프로그램에서 재사용할 수 있습니다.

  2. 중재자 인터페이스는 일반적으로 단일 알림 메서드만을 포함하는 컴포넌트들과의 통신 메서드들을 선언합니다. 컴포넌트들은 자체 객체들을 포함하여 모든 콘텍스트를 이 메서드의 인수로 전달할 수 있지만 이는 수신자 컴포넌트와 발송자 클래스 간의 결합이 발생하지 않는 방식으로만 가능합니다.

  3. 구상 중재자들은 다양한 컴포넌트 간의 관계를 캡슐화합니다. 구상 중재자들은 자신이 관리하는 모든 컴포넌트에 대한 참조를 유지하고 때로는 그들의 수명 주기를 관리하기도 합니다.

  4. 컴포넌트들은 다른 컴포넌트들을 인식하지 않아야 합니다. 컴포넌트 내에서 또는 컴포넌트에 중요한 일이 발생하면, 컴포넌트는 이를 중재자에게만 알려야 합니다. 중재자는 알림을 받으면 발송자를 쉽게 식별할 수 있으며, 이는 응답으로 어떤 컴포넌트가 작동되어야 하는지 결정하기에 충분할 수 있습니다.

    컴포넌트의 관점에서는 모든 것들이 블랙박스들(기능은 알지만, 작동 원리를 이해할 수 없는 복잡한 기계나 시스템)처럼 보입니다. 발송자는 누가 요청을 처리할지 모르고, 수신자는 누가 처음에 요청을 보냈는지를 모릅니다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public interface Mediator {
void addUser(Colleague user);
void deleteUser(Colleague user);
void sendMessage(String message, Colleague user);

}

public abstract class Colleague {
protected Mediator mediator;
protected String name;

public Colleague(Mediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}

public abstract void send(String msg);

public abstract void receive(String msg);
}

public class ConcreteMediator implements Mediator {
private final List<Colleague> users;

public ConcreteMediator() {
this.users=new ArrayList<>();
}

@Override
public void addUser(Colleague user) {
this.users.add(user);
}

@Override
public void deleteUser(Colleague user) {
this.users.remove(user);
}

@Override
public void sendMessage(String message, Colleague user) {
for(Colleague u : this.users){
if(u != user){
u.receive(message);
}
}
}
}

public class ConcreteColleague extends Colleague {
public ConcreteColleague(Mediator mediator, String name) {
super(mediator, name);
}
@Override
public void send(String msg) {
System.out.println(this.name+": Sending Message="+msg);
mediator.sendMessage(msg, this);
}

@Override
public void receive(String msg) {
System.out.println(this.name+": Received Message:"+msg);
}
}

Mediator mediator = new ConcreteMediator();
Colleague user1 = new ConcreteColleague(mediator, "lee");
Colleague user2 = new ConcreteColleague(mediator, "kim");
Colleague user3 = new ConcreteColleague(mediator, "park");
Colleague user4 = new ConcreteColleague(mediator, "yon");
mediator.addUser(user1);
mediator.addUser(user2);
mediator.addUser(user3);
mediator.addUser(user4);

user1.send("안녕하세요");
// lee: Sending Message=안녕하세요
// kim: Received Message:안녕하세요
// park: Received Message:안녕하세요
// yon: Received Message:안녕하세요

mediator.deleteUser(user4);

user2.send("yon없지?");
// kim: Sending Message=yon없지?
// lee: Received Message:yon없지?
// park: Received Message:yon없지?

장단점

장점

  • 단일 책임 원칙. 다양한 컴포넌트 간의 통신을 한곳으로 추출하여 코드를 이해하고 유지 관리하기 쉽게 만들 수 있습니다.
  • 개방/폐쇄 원칙. 실제 컴포넌트들을 변경하지 않고도 새로운 중재자들을 도입할 수 있습니다.
  • 프로그램의 다양한 컴포넌트 간의 결합도를 줄일 수 있습니다.
  • 개별 컴포넌트들을 더 쉽게 재사용할 수 있습니다.

단점

메멘토 패턴(중요X)

사용 상황 요약

  • 메멘토는 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴입니다.

메멘토 패턴 이란?

스크린샷 2023-04-22 오후 5.31.22

  1. 오리지네이터 클래스는 자신의 상태에 대한 스냅샷들을 생성할 수 있으며, 필요시 스냅샷에서 자신의 상태를 복원할 수도 있습니다.

  2. 메멘토는 오리지네이터의 상태의 스냅샷 역할을 하는 값 객체입니다. 관행적으로 메멘토는 불변으로 만든 후 생성자를 통해 데이터를 한 번만 전달합니다.

  3. 케어테이커는 ‘언제’ 그리고 ‘왜’ 오리지네이터의 상태를 캡처해야 하는지 뿐만 아니라 상태가 복원돼야 하는 시기도 알고 있습니다.

    케어테이커는 메멘토들의 스택을 저장하여 오리지네이터의 기록을 추적할 수 있습니다. 오리지네이터가 과거로 돌아가야 할 때 케어테이커는 맨 위의 메멘토를 스택에서 가져온 후 오리지네이터의 복원 메서드에 전달합니다.

  4. 이 구현에서 메멘토 클래스는 오리지네이터 내부에 중첩됩니다. 이것은 오리지네이터가 메멘토의 필드들과 메서드들이 비공개로 선언된 경우에도 접근할 수 있도록 합니다. 반면에, 케어테이커는 메멘토의 필드들과 메서드들에 매우 제한된 접근 권한을 가지므로 메멘토들을 스택에 저장할 수는 있지만 그들의 상태를 변조할 수는 없습니다.

예시

  • Originator.class
    • Originator : 현재 State를 가지고, Memento 객체와 Memento 객체 상태를 얻게 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Originator {
private String state;

public void setState(String state){
this.state = state;
}

public String getState(){
return state;
}

public Memento saveStateToMemento(){
return new Memento(state);
}

public void getStateFromMemento(Memento memento){
state = memento.getState();
}
}
  • Memento class
    • Memento : State를 가지고 있는 인스턴스
1
2
3
4
5
6
7
8
9
10
11
public class Memento {
private String state;

public Memento(String state){
this.state = state;
}

public String getState(){
return state;
}
}
  • CareTaker.class
    • CareTaker : Memento를 순서대로 저장한다
1
2
3
4
5
6
7
8
9
10
11
public class CareTaker {
private List<Memento> mementoList = new ArrayList<Memento>();

public void add(Memento state){
mementoList.add(state);
}

public Memento get(int index){
return mementoList.get(index);
}
}
  • Main.class
    • 저장한 status를 불러올 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 public static void main(String[] args) {
Originator originator = new Originator();
CareTaker careTaker = new CareTaker();
originator.setState("State #1");
originator.setState("State #2");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #3");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #4");

System.out.println("Current State: " + originator.getState());
originator.getStateFromMemento(careTaker.get(0));
System.out.println("First saved State: " + originator.getState());
originator.getStateFromMemento(careTaker.get(1));
System.out.println("Second saved State: " + originator.getState());
}
>>>Current State: State #4
>>>First saved State: State #2
>>>Second saved State: State #3

장단점

장점

  • 캡슐화를 위반하지 않고 객체의 상태의 스냅샷들을 생성할 수 있습니다.
  • 당신은 케어테이커가 오리지네이터의 상태의 기록을 유지하도록 하여 오리지네이터의 코드를 단순화할 수 있습니다.

단점

  • 클라이언트들이 메멘토들을 너무 자주 생성하면 앱이 많은 RAM을 소모할 수 있습니다.
  • 케어테이커들은 더 이상 쓸모없는 메멘토들을 파괴할 수 있도록 오리지네이터의 수명주기를 추적해야 합니다.
  • PHP, 파이썬 및 JavaScript와 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 내의 상태가 그대로 유지된다고 보장할 수 없습니다.

옵서버 패턴

사용 상황 요약

옵서버 패턴은 당신이 여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴입니다.

즉, 이벤트 알리는것에 사용, listener가 추상 구독자 인터페이스에만 의존하므로, 새 구독자 유형을 추가하는것도 쉽다.

publisher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import refactoring_guru.observer.example.listeners.EventListener;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventManager {
Map<String, List<EventListener>> listeners = new HashMap<>();

public EventManager(String... operations) {
for (String operation : operations) {
this.listeners.put(operation, new ArrayList<>());
}
}

public void subscribe(String eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.add(listener);
}

public void unsubscribe(String eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.remove(listener);
}

public void notify(String eventType, File file) {
List<EventListener> users = listeners.get(eventType);
for (EventListener listener : users) {
listener.update(eventType, file);
}
}
}

editor

  • editor가 동작할시 알럿을 준다. 상태변화 추적대상
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import refactoring_guru.observer.example.publisher.EventManager;

import java.io.File;

public class Editor {
public EventManager events;
private File file;

public Editor() {
this.events = new EventManager("open", "save");
}

public void openFile(String filePath) {
this.file = new File(filePath);
events.notify("open", file);
}

public void saveFile() throws Exception {
if (this.file != null) {
events.notify("save", file);
} else {
throw new Exception("Please open a file first.");
}
}
}

listeners

1
2
3
4
5
import java.io.File;

public interface EventListener {
void update(String eventType, File file);
}

EmailNotificationListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.File;

public class EmailNotificationListener implements EventListener {
private String email;

public EmailNotificationListener(String email) {
this.email = email;
}

@Override
public void update(String eventType, File file) {
System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
}
}

LogOpenListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import java.io.File;

public class LogOpenListener implements EventListener {
private File log;

public LogOpenListener(String fileName) {
this.log = new File(fileName);
}

@Override
public void update(String eventType, File file) {
System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
}
}

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import refactoring_guru.observer.example.editor.Editor;
import refactoring_guru.observer.example.listeners.EmailNotificationListener;
import refactoring_guru.observer.example.listeners.LogOpenListener;

public class Demo {
public static void main(String[] args) {
Editor editor = new Editor();
editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

try {
editor.openFile("test.txt");
editor.saveFile();
} catch (Exception e) {
e.printStackTrace();
}
}
}

output

1
2
3
>>>
Save to log \path\to\log\file.txt: Someone has performed open operation with the following file: test.txt
Email to admin@example.com: Someone has performed save operation with the following file: test.txt
CATALOG
  1. 1. 행동 패턴 참조
  2. 2. 책임 연쇄 패턴
    1. 2.1. 사용 상황 요약
    2. 2.2. 책임 연쇄 패턴이란?
    3. 2.3. 예시
      1. 2.3.1. Chain 인터페이스
      2. 2.3.2. Number 클래스
      3. 2.3.3. Process 1,2,3
    4. 2.4. 장단점
      1. 2.4.1. 장점
      2. 2.4.2. 단점
  3. 3. 커맨드 패턴
    1. 3.1. 사용 상황 요약
    2. 3.2. 커맨드 패턴이란?
    3. 3.3. 예시
    4. 3.4. 장단점
      1. 3.4.1. 장점
      2. 3.4.2. 단점
  4. 4. 반복자(중요X)
    1. 4.1. 사용 상황 요약
    2. 4.2. 반복자 패턴 이란?
    3. 4.3. 예시
      1. 4.3.1. BookShelf 예제
      2. 4.3.2. Iterator 패턴을 적용하면 어떤 장점이 있을까?
    4. 4.4. 장단점
      1. 4.4.1. 장점
      2. 4.4.2. 단점
  5. 5. 중재자 패턴
    1. 5.1. 사용 상황 요약
    2. 5.2. 중재자 패턴이란?
    3. 5.3. 예시
    4. 5.4. 장단점
      1. 5.4.1. 장점
      2. 5.4.2. 단점
  6. 6. 메멘토 패턴(중요X)
    1. 6.1. 사용 상황 요약
    2. 6.2. 메멘토 패턴 이란?
    3. 6.3. 예시
    4. 6.4. 장단점
      1. 6.4.1. 장점
      2. 6.4.2. 단점
  7. 7. 옵서버 패턴
    1. 7.1. 사용 상황 요약
    2. 7.2. publisher
    3. 7.3. editor
    4. 7.4. listeners
    5. 7.5. EmailNotificationListener
    6. 7.6. LogOpenListener
    7. 7.7. demo
    8. 7.8. output