카테고리 없음

디자인패턴 - 행위패턴 2

ㅈ현 2022. 8. 21. 03:14

객체지향 프로그래밍을 위한 디자인 패턴중 행위에 관한 패턴을 다룬다.

 

1. ChainOfResponsibility

2. Command

3. Interpreter

4. Iterator

5. Meditator

6. Memento

7. Observer

8. State

9. Strategy

10. TemplateMethod

11. Visitor

 

 

7. Observer

객체의 상태 변화를 관찰자(observer)들에게 알려준다.

옵저버 패턴은 객체의 상태변화를 감지하는 옵저버 객체들을 등록하여, 해당 객체의 변화가 일어나게 되면 등록된 옵저버들에게 객체의 변화를 알려주는 패턴이다. pub-sub 모델로도 알려져 있다.

Observer Class Diagram

Subject 클래스는 Observer객체들을 보관하고 있다. 이후 자신에게 어떠한 변화가 일어나게 되면 notifyObserver()를 통해 자신의 Observer들에게 변화를 알려주게 된다.

 

유튜브 채널(Subject)을 구독한 구독자(Observer)에게 알림(notifyObserver())를 주는것과 같다.

 

Observer.java

public abstract class Observer {
    String state = "";
    public void notify(String state){
        this.state = state;
    }

    public abstract void echo();
}

notify메소드는 Subject로부터 변화된 상태를 공지받는데 사용된다.

ConcreteObserver.java

public class ConcreteObserverA extends Observer {
    public void echo(){
        System.out.printf("ObserverA state : %s\n", this.state);
    }
}

 

Subject.java

public class Subject {
    private String state = "";
    private ArrayList<Observer> clist = new ArrayList();

    public void subscribeObserver(Observer client){
        clist.add(client);
    }

    public void unsubscribeObserver(Observer client){
        clist.remove(client);
    }

    public void notifyObserver(){
        for (Observer client : clist){
            client.notify(this.state);
        }
    }
    public void setState(String state){
        this.state = state;
    }
}

Observer를 추가하는 subscribe, unsubscribe 메소드와 변화를 Observer들에게 공유하는 notifyObserver메소드다.

 

Demo.java

public class Demo {
    public void run() {
        Observer clientA = new ConcreteObserverA();
        Observer clientB = new ConcreteObserverB();

        Subject subject = new Subject();

        subject.subscribeObserver(clientA);
        subject.subscribeObserver(clientB);

        subject.setState("state1");

        subject.notifyObserver();
        clientA.echo();
        clientB.echo();

        subject.unsubscribeObserver(clientA);

        subject.setState("state2");
        subject.notifyObserver();
        clientA.echo();
        clientB.echo();
    }
}

ClientA, B가 모두 구독된 상태에서 상태 변화를 공지하게 되면 clientA,B 모두 상태의 변화를 감지하게 된다.

 

하지만 ClientA를 구독 해제하고, 변화를 공지하게 되면 ClientA는 변화를 알지못하고 ClientB만 변화에 대해 알게된다.

때문에 state2의 변화는 clientA는 알지 못하고 ClientB만 알게된다.

 

Output

ObserverA state : state1
ObserverB state : state1
ObserverA state : state1
ObserverB state : state2

 

8. State

객체의 상태와 각 상태별로 행동할 행위에 대해 정의하게 된다.

객체에 다양한 상태를 부여하고, 각 상태에 따라 동작하는 행위를 다르게 정의한다.

State Class Diagram

Context객체는 State객체를 자신의 상태로 가지게 된다. State객체는 각 ConcreteState마다 다른 operation 동작을 가지게 된다.

 

컴퓨터(Context)는 전원에 대한 상태(State)를 가진다.
상태로는 전원이 꺼져있는 상태(ConcreteState1)와, 전원이 켜져있는 상태(ConcreteState2)를 가진다.
컴퓨터의 상태(Context.state)가 전원이 꺼져있는 상태(ConcreteState1)에서 전원버튼을 누르면(Context.stateOperation())
전원이 켜지는 행위가 일어나게 되며 컴퓨터의 상태는 전원이 켜져있는 상태(ConcreteState2)로 바뀌게 된다.
이상태에서 동일한 전원 버튼을 누르게 되면(Concrete.stateOperation()) 같은 전원버튼이지만, 전원이 꺼지는 행위가 일어나게 된다.
이후 컴퓨터의 상태는 전원이 꺼져있는 상태(ConcreteState1)로 바뀌게 된다. 

 

State.java

public interface State {
    public void operation(Context c);
}

상태를 정의한다. 각 상태는 operation이라는 행위를 가진다.

 

ConcreteState1.java

public class ConcreteState1 implements State{

    private static ConcreteState1 instance = null;
    private ConcreteState1() {}
    public static ConcreteState1 getInstance(){
        if (instance == null) {
            instance = new ConcreteState1();
        }
        return instance;
    }

    public void operation(Context c){
        System.out.println("state1 => state2");
        c.setState(ConcreteState2.getInstance());
    }
}

 

각 State는 싱글턴으로 구현되며, 3가지 상태가 존재한다.

각 상태에서의 동작은 다음과 같다.

state1에서의 operation : state1 => state2로 상태 변화
state2에서의 operation : state2 => state3로 상태 변화
state3에서의 operation : state3 => state1로 상태 변화

 

 Context.java

public class Context {
    private State state;

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

    public void stateOperation(){
        state.operation(this);
    }
}

 

Demo.java

public class Demo {
    public void run(){
        Context c = new Context();
        c.setState(ConcreteState1.getInstance());

        c.stateOperation();
        c.stateOperation();
        c.stateOperation();
        c.stateOperation();
    }
}

 Context는 총 4번의 Operation이 일어나게 된다. 

각 operation마다 Context의 상태를 변화시키게 된다.

Output

state1 => state2
state2 => state3
state3 => state1
state1 => state2

 

 

9. Strategy

전략에 따라 실행되는 알고리즘을 바꿀 수 있게 한다.

Strategy패턴은 실행 알고리즘을 캡슐화 하여 전략에 따라 실질적으로 실행되는 알고리즘을 바꿀 수 있도록 하는 패턴이다.

 

Strategy Class Diagram

 

클래스 다이어그램은 State 패턴과 동일하다.

 

재현(Context)는 A 장소에서 B 장소까지 가야한다.
이를 위한 전략(Strategy)을 세워 가도록 한다.
걸어가는 전략 (ConcreteStrategy1)을 세운다면 재현은 A에서 B까지 걸어가게 된다.
뛰어가는 전략 (ConcreteStrategy2)를 세운다면 재현은 A에서 B까지 뛰어가게 된다.

 

Strategy.java

public interface Strategy {
    public void algorithm();
}

 

ConcreteStrategy.java

public class ConcreteStrategyA implements Strategy {
    public void algorithm(){
        System.out.println("전략 A 알고리즘 적용");
    }
}

전략에대한 실질적인 알고리즘을 정의한다.

 

Context.java

public class Context {
    private Strategy strategy;

    public void operation(){
        strategy.algorithm();
    }

    public void setStrategy(Strategy moveableStrategy){
        this.strategy = moveableStrategy;
    }
}

전략을 필드로 가지며, 각 전략에 따라 실행 알고리즘이 달라진다.

 

Demo.java

public class Demo {
    public void run(){
        Context context = new Context();

        context.setStrategy(new ConcreteStrategyA());
        context.operation();

        context.setStrategy(new ConcreteStrategyB());
        context.operation();
    }
}

Context의 전략을 수정하여 각 전략에 따른 동작을 수행한다.

 

Output

전략 A 알고리즘 적용
전략 B 알고리즘 적용

 

Command, State, Strategy 패턴간 차이

이 세 패턴은 사용법도 비슷하며 예시 코드나, 클래스 다이어그램으로 봐도 비슷한 면이 많다. 

하지만 사용법에 대해 조금씩 차이를 보인다.

 

command는 어떠한(what) 명령을 사용할까에 중점을 둔다.

strategy는 어떻게(how) 명령을 수행할까에 중점을 둔다.
state는 어떠한 상태에서 어떠한 동작을 할까에 중점을 둔다.

 

예를들어 TV를 조작하는 환경이라 한다면

 

command는 볼륨을 줄일지, 채널을 바꿀지

strategy는 TV의 볼륨을 줄일때, TV를 직접 조작하여 줄일지, 리모컨을 사용하여 줄일지

state는 똑같은 리모컨의 전원 버튼을 누르더라도 TV가 꺼져있다면 켜지는 동작을, TV가 켜져있다면 꺼지는 동작을 수행하듯

 

세가지 패턴은 비슷하면서 다른 사용 방법을 가진다.

 

 

 

10. TemplateMethod

동작의 뼈대를 정의하고, 동작의 세부적인 부분은 재정의하여 사용한다.

템플릿 메소드는 어떠한 동작의 기초적인 뼈대를 정의하고, 이후 그 뼈대를 이루는 세부동작을을 재정의하여 사용할 수 있도록 하는 패턴이다.

 

TemplateMethod Class Diagram

아래 예시코드에서 다루겠지만, AbstractClass.templateMethod()는 primitiveMethod()들을 실행하는 메소드다.

즉 커다란 틀을 정의하고 내부적인 동작은 하위 ConcreteClass에서 정의하여 사용하는 패턴이다.

 

 

AbstractaClass.java

public abstract class AbstractClass {
    public void templateMethod(){
        primitiveMethod1();
        primitiveMethod2();
    }
    public abstract void primitiveMethod1();
    public abstract void primitiveMethod2();

}

 

ConcreteClass.java

public class ConcreteClass1 extends AbstractClass {
    public void primitiveMethod1(){
        System.out.println("클래스1의 메소드1");
    }

    public void primitiveMethod2(){
        System.out.println("클래스1의 메소드2");
    }
}

ConcreteClass에서 primitiveMethod를 재정의하여 사용한다.

 

 

Demo.java

public class Demo {
    public void run(){
        ConcreteClass1 class1 = new ConcreteClass1();
        ConcreteClass2 class2 = new ConcreteClass2();

        class1.templateMethod();
        class2.templateMethod();
    }
}

ConcreteClass1의 경우 "클래스1의 메소드1", "클래스1의 메소드2" 를 출력하는 동작으로 재정의 하였고,
ConcreteClass2의 경우 "클래스2의 메소드1", "클래스2의 메소드2" 를 출력하는 동작으로 재정의 하여 사용한다.

 

Output

클래스1의 메소드1
클래스1의 메소드2
클래스2의 메소드1
클래스2의 메소드2

 

 

11. Visitor

객체의 확장을 위해 로직을 객체에서 분리하여 사용한다.

Visitor 패턴의 경우 로직을 Visitor라는 객체로 분리하여 사용한다.

 

Visitor Class Diagram

각 객체(Element)들은 각자의 로직을 필요로 하게 된다.

하지만 이 로직들을 각 객체에 구현하지 않고 Visitor라는 객체(ConcreteVisitor)에 구현하여, 

해당 객체가 Element를 방문(Visit)함으로 로직이 이뤄진다.

이러한 Visitor의 방문은 원 객체(Element)의 허가(accept)로 이루어진다.

 

Element.java

public abstract class Element {
    String name = "";
    public Element(String name){
        this.name = name;
    }
    public String getName(){
        return this.name;
    }
    abstract public void accept(Visitor visitor);
}

 

ConcreteElement1.java

import java.util.ArrayList;

public class ConcreteElement1 extends Element {
    public ArrayList<Element> ilist = new ArrayList();
    public ConcreteElement1(String name){
        super(name);
    }

    public void accept(Visitor visitor){

        visitor.visit(this);
        for(Element i : ilist){
            i.accept(visitor);
        }
    }

    public void addItem(Element i){
        ilist.add(i);
    }
}

 

ConcreteElement2.java

public class ConcreteElement2 extends Element {
    public ConcreteElement2(String name){
        super(name);
    }
    public void accept(Visitor visitor){
        visitor.visit(this);
    }
}

이 예시에서는 ConcreteElement1은 또다른 Element들을 가질 수 있는 파일시스템의 디렉토리와 같이 동작하고,

ConcreteElement2는 마치 파일시스템의 파일과 같이 동작한다.

 

Visitor.java

public interface Visitor {
    public void visit(ConcreteElement1 d);
    public void visit(ConcreteElement2 f);
}

각 ConcreteElement1,2에 대한 visit 을 정의한다.

 

ConcreteVisitor.java

public class ConcreteVisitor implements Visitor{
    public void visit(ConcreteElement1 d){
        System.out.println("element1 탐색 " + d.getName());
    }
    public void visit(ConcreteElement2 f){
        System.out.println("element2 탐색 " + f.getName());
    }
}

각 Element에서 실제로 수행할 로직을 정의한다.

 

Demo.java

public class Demo {
    public void run(){
        ConcreteVisitor v = new ConcreteVisitor();
        ConcreteElement1 d1 = new ConcreteElement1("d1");
        ConcreteElement1 d2 = new ConcreteElement1("d2");

        ConcreteElement2 f1 = new ConcreteElement2("f1");
        ConcreteElement2 f2 = new ConcreteElement2("f2");
        ConcreteElement2 f3 = new ConcreteElement2("f3");

        d1.addItem(f1);
        d2.addItem(f2);
        d2.addItem(f3);

        d1.addItem(d2);
        d1.accept(v);
    }
}

이 예시에서 Element1은 마치 디렉토리처럼, Element2는 마치 파일처럼 동작한다.

 

Output

element1 탐색 d1
element2 탐색 f1
element1 탐색 d2
element2 탐색 f2
element2 탐색 f3

 

 

7. Observer

8. State

9. Strategy

10. TemplateMethod

11. Visitor