command의 의미는 '명령어'이다. 문서편집기의 복사(copy), 붙여넣기(paste), 잘라내기(cut) 등도 모두 명령어이다. 그런데 이런 명령어를 각각 구현하는 것보다는 [그림 5-52]처럼 하나의 추상 클래스 execute() 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는 서브 클래스(copy_command)가 선택되어 실행하는 것이 효율적이다. 이는 함수 오버로딩(overloading)과 같은 추상화 개념을 사용한 것이다.

그러나 command 패턴은 단순히 명령어를 추상 클래스(abstract class)와 구체 클래스(copy_command,cut_command, paste_command)로 분리하여 단순화한 것으로 끝나지 않고, 명령어에 따른 취소(undo) 기능까지도 포함한다(사용자 입장에서는 해당 명령어를 실행했다가 취소(undo)하기도 하기 때문이다). 이렇게 프로그램의 명령어를 구현할 때는 command 패턴을 활용할 수 있다.

 

이러한 커맨드 패턴은 아주 예전에 사용하던 servlet에서도 사용하던 패턴입니다. FrontController를 최앞단에 두고 FrontController에서 모든 요청을 다 받은 후 커맨드 패턴을 이용하여 분기처리했습니다. 즉, 이렇게 어떠한 이벤트에 대해 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할때 유용한 패턴입니다.

 

만능 리모콘을 예제로 살펴보겠습니다. 리모콘은 실내의 전등을 키고 끄는 버튼이 있고 TV를 켜고 끄는 버튼도 있습니다. 추후에는 다양한 전자기기를 다룰 수 있는 버튼이 추가될 가능성이 있습니다. 이럴 경우에는 어떻게 커맨드 패턴을 적용해 볼 수 있을까요? 아래와 같이 쉽게 구현은 가능합니다.

 

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
public class SimpleRemoteController {
    
    private TV tv;
    private Light light;
    private Mode mode;
    
    enum Mode{
        TV,LIGHT;
    }
    
    public SimpleRemoteController(TV tv, Light light) {
        this.tv=tv;
        this.light=light;
    }
    
    public void setMode(Mode mode) {
        this.mode=mode;
    }
    
    public void pressOnButton() {
        if(this.mode.equals(Mode.TV)) {
            tv.turnOn();
        }else if(this.mode.equals(Mode.LIGHT)) {
            light.turnOff();
        }
    }
    
    ...
    
}
cs

이 코드에는 분명 문제가 있습니다. SOLID 원칙 중 분명 OCP에 위반되기 때문이죠. 확장에는 열려있고 변경에는 닫혀있어야 하는 원칙을 누가 봐도 어기고 있습니다. 기능이 추가될때마다 코드 변경이 필요하기 때문이죠. 하지만 이것을 커맨드 패턴을 이용해 구현한다면 리모콘을 컨트롤하는 클래스는 어떠한 행위에 대한 구체적인 구현을 몰라도 되며 구체적인 행동을 캡슐화하고 있는 커맨드 객체만 받으면 되기에 훨씬 확장이 수월한 코드가 나오게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RemoteControlInvoker {
    
    private Command command;
    
    public RemoteControlInvoker(Command command) {
        this.command=command;
    }
 
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void pressedButton() {
        command.execute();
    }
    
}
cs

 

이제는 리모콘 역할을 하는 클래스에는 구체적인 행위가 정의되어 있지 않습니다. 생성자의 매개변수 혹은 setter를 이용하여 주입받은 커맨드 객체로 행위를 호출하는 호출자(Invoker) 역할을 하게됩니다.

 

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
public interface Command {
    public void execute();
}
 
public class TvOnCommand implements Command {
    
    private TV tv;
    
    public TvOnCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOn();
    }
 
}
 
public class TvOffCommand implements Command {
    
    private TV tv;
    
    public TvOffCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOff();
    }
 
}
 
public class LightOnCommand implements Command {
    
    private Light light;
    
    public LightOnCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOn();
    }
 
}
 
public class LightOffCommand implements Command {
    
    private Light light;
    
    public LightOffCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOff();
    }
 
}
 
cs

 

Command라는 인터페이스를 만들고 execute()라는 메소드를 선언합니다. 그리고 구체적인 요청(행위)을 하나의 커맨드 객체로 캡슐화 합니다. 해당 커맨드 객체는 요청을 커맨드 객체에게 간접적으로 받아서 처리하는(Receiver)를 한번 감싸는 행위 캡슐화 역할을 하게 됩니다.

특이하게 이제는 클라이언트 입장에서 어떠한 행위든지 상관없이 execute()만 호출하면 되는 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TV {
    
    public void turnOn() {
        System.out.println("TV 켰음");
    }
    public void turnOff() {
        System.out.println("TV 껐음");
    }
}
 
public class Light {
    
    public void turnOn() {
        System.out.println("불 켰음");
    }
    public void turnOff() {
        System.out.println("불 껐음");
    }
}
 
 
cs

 

리시버 객체의 구현입니다.

 

이제 리모콘 역할을 하는 Invoker는 어떠한 기능이 추가되더라도 코드 변경없이 기능을 얼마던지 추가 할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        RemoteControlInvoker invoker = new RemoteControlInvoker(tvOnCommand);
        invoker.pressedButton();
        invoker.setCommand(tvOffCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOnCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOffCommand);
        invoker.pressedButton();
        
    }
 
}
cs

 

마지막으로 일련의 행동을 연속적으로 처리할 수 있는 커맨드 패턴은 어떻게 구현할까 생각해볼 수 있습니다. 이런 패턴을 매크로 커맨드 패턴이라고 합니다. 구현은 간단히 아래와 같이 진행할 수 있습니다.

 

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
public class MacroCommand implements Command {
    
    private List<Command> commands;
    
    public MacroCommand() {
        commands= new ArrayList<>();
    }
    
    public void addCommand(Command...commands) {
        if(commands.length < 1throw new NullPointerException();
        
        for(Command c : commands) {
            this.commands.add(c);
        }
    }
    
    @Override
    public void execute() {
        if(commands.size()>0) {
            commands.stream().forEach(c->c.execute());
        }
    }
 
}
 
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        MacroCommand macroCommand = new MacroCommand();
        macroCommand.addCommand(tvOnCommand,
                                tvOffCommand,
                                lightOnCommand,
                                lightOffCommand);
        RemoteControlInvoker invoker = new RemoteControlInvoker(macroCommand);
        invoker.pressedButton();
        
    }
 
}
cs

 

여기까지 간단한 커맨드 패턴 예제를 다루어봤습니다. 사실 커맨드 패턴은 위보다도 더욱 복잡하게 사용가능한 패턴입니다. 예를 들어 상태값을 가져 상태에 따른 행위를 할 수 있고, 또한 확장하여 일련의 작업들을 작업 큐에 넣어 쓰레드들이 각각 하나의 Command를 맡아서 작업을 할 수도 있는 등 구현의 범위는 넓습니다.

posted by 여성게
:

state의 의미는 '상태'이다. 엘리베이터의 정지, 하강, 상승 상태처럼 객체 상태도 상황에 따라 달라진다. state 패턴은 동일한 동작을 객체 상태에 따라 다르게 처리해야 할 때 사용한다.

이렇게 하나의 객체에 여러 가지 상태(예, 정지, 상승, 하강)가 존재할 때 패턴을 사용하지 않고 프로그래밍을 하면 if 문 또는 switch 문을 사용하여 처리한다. 그런데 신규 상태(예, 문 열림, 문 닫힘)가 발생하면 프로그램을 다시 수정해야 한다.

이런 경우에 state 패턴은 그림처럼 객체 상태를 캡슐화하여 클래스화(state 인터페이스)함으로써 그것을 참조하게 하는 방식으로 상태에 따라 다르게 처리(upState, stopState, downState)할 수 있도록 한 것이다. 따라서 변경 시(신규 상태 추가) 원시 코드의 수정을 최소화할 수 있고, 유지보수를 쉽게 할 수 있다.

 

형광등을 예로 들어보자. 형광등의 첫 상태는 스위치가 꺼진 OFF 상태라고 생각하자. 상태가 OFF인 상태에서 스위치를 ON하면 불이 켜질 것이다. 그렇지만 상태가 ON인 상태에서 스위치를 ON하면 이미 불이 켜진 상태라 아무런 동작을 하지 않을 것이다. 그 반대도 역시 동일하다. 그렇다면 이렇게 동작하는 형광등 클래스는 어떻게 설계하면 좋을까? 가장 간단한 구현은 아래와 같을 것이다.

 

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
public class NotStatePattern {
    
    public static final int ON=1;
    public static final int OFF=0;
    
    //현 상태
    private int currentState;
    
    public NotStatePattern() {
        currentState=OFF;
    }
    
    //ON 스위치 push
    public void on_button() {
        if(currentState==ON) System.out.println("동작 없음.");
        else {
            System.out.println("불 켜짐!");
            currentState=ON;
        }
    }
    //OFF 스위치 push    
    public void off_button() {
        if(currentState==OFF) System.out.println("동작 없음.");
        else {
            System.out.println("불 꺼짐!");
            currentState=OFF;
        }
    }
    
}
cs

 

ON,OFF 상태값을 가지는 상수와 현재 형광등의 상태를 가지고 있는 변수. 그리고 스위치를 ON,OFF할때 발생되는 행위를 메소드로 구현해놓았다. 얼핏 봤을 때, 나쁘지 않은 구현일 수 있다. 하지만 예를 들어 수면모드가 들어간 형광등이라면? 스위치가 ON인 상태에서 다시 한번 ON 스위치를 누를 경우 수면 모드로 들어간다. 그리고 수면 모드에서 OFF 스위치를 누르면 형광등이 꺼진다. 만약 복잡한 시스템이라면 이러한 상태값은 더 많이 갖게 될 것이고, 그럼 많은 상태에 따른 행위를 위와 같이 if문 혹은 switch 문으로 다뤄야할 것이며 이는 상태가 추가될때 마다 코드가 수정되야하고 가독성도 떨어지며 최종적으로는 유지보수가 힘든 코드가 될 것이다.

 

이러한 상황에서 사용할 수 있는 디자인 패턴이 스테이트 패턴(State Pattern)이다. 아마 예제 코드를 본다면 우리 이미 다루어본 스트레티지 패턴(전략패턴)과 비슷하다 볼 수 있을 것이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Light {
    
    private State state;
    
    public Light() {
        state=OffState.getInstance();
    }
    
    public void setState(State state) {
        this.state=state;
    }
    
    public void onButton() {
        this.state.onButton(this);
    }
    
    public void offButton() {
        this.state.offButton(this);
    }
    
}
cs

 

형광등 클래스이다. 마치 스트레티지 패턴의 Context 객체와 비슷하다. 즉, 해당 객체는 실제 알고리즘을 수행하는 실체 객체를 알지못한다. 단순히 남에게 행위를 위임해주는 역할이다.

 

1
2
3
4
5
6
public interface State {
    
    public void onButton(Light light);
    public void offButton(Light light);
    
}
cs

 

디자인 원칙을 떠올려보자. 변하는 것은 잘 변하지 않는 것과 분리해라. 즉, 변하는 녀석들을 캡슐화해라! 

 

우리는 변하는 것은 상태(State)라는 것을 알고있다. 이러한 상태를 인터페이스로 분리시켰고 각 상태에 따른 구현 클래스를 만들어 줄 것이다. 여기서 스트래티지 패턴과 조금 다른 점은 Light(Context)객체가 행위를 위임할때 자신의 상태를 변경하기 위하여 자기자신을 메소드 매개변수로 넘기는 것을 주목하자! 차이점은 마지막에 다룰 것이다.

 

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
public class OnState implements State {
    
    private OnState() {}
    
    private static class LazyHolder{
        public static final OnState ON=new OnState();
    }
    
    public static OnState getInstance() {
        return LazyHolder.ON;
    }
 
    @Override
    public void onButton(Light light) {
        light.setState(SleepState.getInstance());
        System.out.println("잠자기 모드!");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
 
public class OffState implements State {
    
    private OffState() {}
    
    private static class LazyHolder{
        public static final OffState OFF=new OffState();
    }
    
    public static OffState getInstance() {
        return LazyHolder.OFF;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("불 켜짐!");
    }
 
    @Override
    public void offButton(Light light) {
        System.out.println("동작 없음!");
    }
 
}
 
public class SleepState implements State {
    
    private SleepState() {}
    
    private static class LazyHolder{
        public static final SleepState SLEEP=new SleepState();
    }
    
    public static SleepState getInstance() {
        return LazyHolder.SLEEP;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("잠자기 모드 해제!(ON 상태)");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
cs

 

각 상태들의 구현체이다. 여기서 조금 특이한 것은 각 상태의 구현체들이 싱글톤으로 구현되어 있다는 것이다. 이것은 상태들이 행위를 수행하면서 Light 객체의 상태를 수시로 바꾸어주기 때문에 싱글톤으로 작성하지 않으면 매번 새로운 인스턴스가 생겨 불필요한 메모리를 잡아 먹을 것이고 전체적으로 성능 저하의 원인이 될 것이기 때문에 싱글톤으로 작성하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StatePatterMain {
 
    public static void main(String[] args) {
        Light l = new Light();
        
        l.offButton();
        l.onButton();
        l.onButton();
        l.onButton();
        l.offButton();
        l.onButton();
        l.offButton();
    }
 
}
 
=>result
동작 없음!
불 켜짐!
잠자기 모드!
잠자기 모드 해제!(ON 상태)
불 꺼짐!
불 켜짐!
불 꺼짐!
cs

 

마지막 결과이다. 잘 보면 스트래티지 패턴과 차이점이 보일 것이다. 스트래티지 패턴을 떠올려보면 클라이언트 쪽에서 알고리즘을 변경하기 위하여 setter를 호출해 직접 수행할 알고리즘을 주입해주었던 것을 기억할 것이다. 즉, 클라이언트가 구체적인 알고리즘의 수행까지는 몰라도 어느정도 무엇무엇이 있는지 정도는 알고 있어야 한다는 것이다. 하지만 스테이트 패턴은 각 상태 구현 클래스들이 자신들의 행위를 수행하면서 직접 Context(Light)객체의 상태를 변경해주기 때문에 클라이언트 입장에서는 직접 상태를 조작하거나 하지 않아도 된다는 점이다. 즉, 클라이언트는 상태를 몰라도 된다라는 뜻이다.(전략을 직접 클라이언트가 바꿔서 사용해야하는 스트래티지 패턴과는 조금은 상반된다.)

 

즉, 용도가 조금 다른 패턴이라고 볼 수 있다.

 

스트래지티 패턴 - 사용자가 쉽게 알고리즘 전략을 바꿀 수 있도록 유연성을 제공. 상속의 한계를 해결하기 위하여 나온 패턴

스테이트 패턴 - 한 객체가 동일한 동작을 상태에 따라 다르게 수행해야 할 경우 사용하는 패턴

posted by 여성게
:

 

어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해주는 것이 observer 패턴이다. 그림을 보면 이 패턴은 외부 객체의 상태 변화에 따라(subject 클래스) observer 객체(observer 인터페이스 클래스)는 이에 상속되어 있는 다른 객체(concreteObserver)들에게 변화된 상태를 전달하고(notify 메서드) 상속된 객체들은 그에 맞게 기능을 수행하는 형태로 구성된다.

이 패턴에 해당되는 예는 관심 있는 어떤 블로그를 미리 등록해놓으면 그 블로그에 새 글이 올라올 때마다 자동으로 알려주는 것과 같다. 즉 observer 패턴은 어떤 일이 생기면 미리 등록한 객체들에게 상태 변화를 알려주는 역할을 한다.

 

다시 말하자면 Subject 객체의 상태에 변화가 있을 때, Observer들에게 상태변화를 알린다. 그 이후 Observer들은 상태변화에 맞게 행동을 취한다. 우리가 잘 알고 있는 신문사 구독과 비슷한 이론인 것이다. 어떤 고객이 신문사에 구독을 신청하면 신문사가 새로운 신문을 발행할때마다 고객에게 신문을 보내는 것과 같은 행동이다.

 

예를 들어 온도,습도,기압 등의 기상 상태를 센서로부터 수집하는 Subject가 있다고 생각하고, 해당 Subject는 기상 상태의 변화가 있을때, Observer들에게 변화를 알리고 Observer들은 기상 상태 변화에 따른 새로운 데이터를 디스플레이에 출력하는 것을 구현하는 상황이다.

 

여기서 적용할 수 있는 패턴이 옵저버 패턴이다. 바로 구현해보자.(예제는 단순히 온도 변화만 다루었다.) 그리고 구독을 해지하는 등의 코드도 구현하지 않았고 상태변화가 있을 때, 옵저버들에게 알려주는 기능만 구현하였다.

 

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
public class Subject extends Observable{
    
    private int temp;
    
    public void changeTemp(int temp) {
        setChanged();
        notifyObservers(temp);
    }
    
    public void changeTemp() {
        setChanged();
        notifyObservers();
    }
    
    public void setChangeTemp(int temp) {
        this.temp = temp;
        changeTemp(temp);
    }
 
    public int getTemp() {
        return temp;
    }
 
    public void setTemp(int temp) {
        this.temp = temp;
        changeTemp();
    }
    
}
 
cs

 

Subject 클래스이다. 이미 자바에는 옵저버 패턴의 구현을 쉽게 해주는 API를 제공하고 있다. Subject를 구현하기 위해서는 Observable 클래스를 상속받아 확장하여 사용하면 된다.(사실 Observable을 확장하면 단점들이 존재한다. 반드시 상속을 강요하고 있으며 추상클래스가 아닌 일반 클래스로 되어 있기에 나중에 필요한 기능을 확장하기 위하여 상속을 받을 수 없다. 왠만하면 새로 구현해서 사용하는 것도 추천한다.)

 

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
public class Observer1 implements Observer {
    
    private Observable sub;
    private int temp;
    
    public Observer1(Observable sub) {
        this.sub=sub;
        sub.addObserver(this);
    }
 
    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof Subject) {
            if(arg != nullthis.temp=(int)arg;
            else this.temp=((Subject) o).getTemp();
        }
        
        display();
 
    }
    
    public void display() {
        System.out.println("Observer1 - "+temp);
    }
 
}
 
public class Observer2 implements Observer {
    
    private Observable sub;
    private int temp;
    
    public Observer2(Observable sub) {
        this.sub=sub;
        sub.addObserver(this);
    }
 
    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof Subject) {
            if(arg != nullthis.temp=(int)arg;
            else this.temp=((Subject) o).getTemp();
        }
        display();
    }
 
    public void display() {
        System.out.println("Observer2 - "+temp);
    }
    
}
cs

 

Observer라는 인터페이스를 상속한 Observer 클래스 2개를 구현하였다. 해당 인터페이스에는 Subject에서 호출할 update 메소드만 오버라이드해서 구현해주면 된다. 옵저버 구현체들에서는 push 방식으로 데이터를 매개변수(arg)로 받아도 되지만 Subject 클래스에서 상태값에 대한 getter를 열어주어서 Observer 구현체들이 pull(getter호출)해서 가져가도 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ObserverMain {
 
    public static void main(String[] args) {
        
        Subject sub= new Subject();
        
        Observer observer1 = new Observer1(sub);
        Observer observer2 = new Observer2(sub);
        
        sub.changeTemp(20);
        sub.setTemp(10);
    }
 
}
 
=>결과
Observer2 - 20
Observer1 - 20
Observer2 - 10
Observer1 - 10
cs

 

결과적으로 Observer 구현체들의 어떠한 메소드도 호출하지 않았음에도 불구하고 Subject 클래스의 상태가 바뀔 때마다 Observer의 메소드가 호출되어 동작한다. 이러한 패턴이 옵저버 패턴이다. 이러한 패턴을 다른 말로 pub/sub 패턴이라고 부르기도 한다. 이러한 옵저버 패턴을 자바는 내부적으로 비동기처리 API의 내부 구현로직으로 사용하기도 한다.

 

위의 구현을 보면 Observer 구현체들의 인스턴스 변수로 Observable 인터페이스가 선언되어 있는 이유는 우선 생성자가 호출될때 해당 Observable 변수가 초기화되고, 이후에 별로 쓰이지 않는 듯 보이지만, 필자가 이야기 했듯 해당 예제에는 구독을 해지하는 등의 로직이 포함되어 있지않다. 추후에 구독을 해지하는 로직을 Observer 구현체들에게 메소드 형태로 넣어주고 내부적으로 Observable의 deleteObserver를 호출하게 하면 된다.

posted by 여성게
:

 

스트레티지 패턴(Strategy Pattern,전략패턴)은 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만들어준다. 스트레티지 패턴을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 

바로 예제를 살펴보자.

 

Duck이라는 추상클래스가 있다고 생각해보자. 해당 추상클래스는 다양한 오리라는 서브클래스를 만들기 위해 여러가지 공통 메소드와 확장을 위한 추상 메소드를 가진 추상클래스이다. 만약 아래와 같은 요청이 있다고 생각해보자.

 

"모든 오리에게 날 수 있는 기능과 오리소리를 내는 기능을 추가해라. 하지만 구현체마다 해당 행동들은 모두 달라질 수 있다"

 

어떤 식으로 구현을 하면 좋을까? 두개의 행동이 모든 구현체마다 달라질 수 있다는데, 추상클래스에서 추상메소드로 만들어줘서 모든 서브 클래스에서 오버라이드 할 수 있게 할까? 그렇다면 어떠한 문제가 생길수 있을까 생각해보자. 문제는 이것이다. 서브 클래스에서 나는 행동과 오리소리를 내는 행동을 오버라이드 한다면 과연 이러한 코드는 재사용이 가능할까? 문제를 다시 한번보자. 

 

"구현체마다 행동들은 모두 달라질 수 있다"

 

달라질 수 있다지, 모든 구현체마다 다르다는 아니다. 이 말은 종류가 다른 오리지만 행동이 같은 교집합이 존재할 수 있다는 것이다. 그렇다면 서브클래스에서 슈퍼클래스의 나는 행동과 오리소리를 내는 행동을 오버라이드한다면 같은 행동을 갖는 다른 서브 클래스들이 동일한 코드가 중복되서 나타날 것이다. 그러면 다음 해결책을 보자.

 

"달라지는 행동들을 별도의 인터페이스로 분리하여 해당 기능이 필요한 오리 서브 클래스에 구현을 강제하자."

 

이 해결책 또한 재활용하기 힘들다. 서브 클래스에서 인터페이스의 구현을 강제하여 나는 행동과 오리소리 행동을 구현한다면 이 역시 재활용할 수 없는 코드가 될 것이다. 즉, 구현 클래스에서만 구현코드가 나타나기 때문에 행동이 같은 다른 서브 클래스에서 해당 코드를 재활용할 수 없다.

 

여기서 사용할 수 있는 디자인 패턴이 스트래티지 패턴(Strategy Pattern)이다. 이 패턴의 핵심은 달라지는 행동부분을 분리하고 캡슐화하여 클라이언트와는 독립적으로 행동을 주입할 수 있게 하는 것이다. 바로 구현 코드를 살펴보자.

 

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
public abstract class Duck {
    
    private FlyBehavior fly;
    private QuackBehavior quack;
    
    public void swim() {
        System.out.println("헤엄친다.");
    }
    
    public void fly() {
        fly.fly();
    }
    
    public void quack() {
        quack.quack();
    }
 
    public void setFly(FlyBehavior fly) {
        this.fly = fly;
    }
 
    public void setQuack(QuackBehavior quack) {
        this.quack = quack;
    }
    
}
cs

 

위의 추상 클래스를 살펴보면 이 추상클래스는 다양한 오리의 구현체의 공통 행동들을 담은 추상클래스이다. 그리고 오리 구현체들마다 달라질 수 있는 행동을 인터페이스로 분리하여 캡슐화하고 있다. 이 말은 상속이나 구현을 강제하는 것이 아니고 인스턴스 변수로 달라지는 행동을 구성(composition)하였다. 그리고 달라지는 행동들은 인터페이스를 구현하여 특정 클래스의 군(알고리즘군)으로 만들어지기 때문에 같은 행동을 하는 다른 오리들이 같은 클래스를 사용하므로 재사용성이 높아진다.

 

1
2
3
4
5
6
7
public interface FlyBehavior {
    public void fly();
}
 
public interface QuackBehavior {
    public void quack();
}
cs

 

달라지는 행동들은 인터페이스로 분리하였다.

 

1
2
3
4
5
6
7
public class ADuck extends Duck{
 
}
 
public class BDuck extends Duck{
 
}
cs

 

추상클래스를 상속받은 오리서브 클래스들은 만들었다. 특징은 ADuck은 헤엄은 칠 수 있지만 날지 못하고 오리소리를 내지 못한다. BDuck은 수영도 가능하고 날수도 있고 울수도 있다. 과연 이러한 달라진 행동들을 실제로 어떻게 사용할까?

 

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
public class NotFlyBehavior implements FlyBehavior {
 
    @Override
    public void fly() {
        System.out.println("못 날아요");
    }
 
}
 
public class DoFlyBehavior implements FlyBehavior {
 
    @Override
    public void fly() {
        System.out.println("난다.");
    }
 
}
 
public class NotQuackBehavior implements QuackBehavior {
 
    @Override
    public void quack() {
        System.out.println("못 운다.");
    }
 
}
 
public class DoQuackBehavior implements QuackBehavior {
 
    @Override
    public void quack() {
        System.out.println("운다.");
    }
 
}
 
 
cs

 

구체적인 행동들을 구현한 클래스 집합군들이다.(나는 행동 집합, 오리소리는 내는 행동 집합)

 

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
/*
 * ADuck은 날지못하고 울지도 못한다.
 * BDuck은 날수 있고 울 수도 있다.
 */
public class DuckMain {
 
    public static void main(String[] args) {
        
        Duck a = new ADuck();
        a.setFly(new NotFlyBehavior());
        a.setQuack(new NotQuackBehavior());
        
        
        Duck b = new BDuck();
        b.setFly(new DoFlyBehavior());
        b.setQuack(new DoQuackBehavior());
        
        System.out.println("===========ADuck===========");
        
        a.swim();
        a.fly();
        a.quack();
        a.setFly(new DoFlyBehavior());
        a.fly();
        
        System.out.println("===========BDuck===========");
        
        b.swim();
        b.fly();
        b.quack();
    }
 
}
 
=>결과
===========ADuck===========
헤엄친다.
못 날아요
못 운다.
난다.
===========BDuck===========
헤엄친다.
난다.
운다.
 
cs

 

이렇게 사용 시점에 알고리즘을 set 메소드로 주입하여 사용한다. 만약 추후에 ADuck이 나는 기능이 추가된다면 위 코드와 같이 날수 있는 구현 클래스를 주입해주기만 하면 ADuck은 날 수 있게 된다. 즉, ADuck은 행동을 인터페이스로 의존만 하고 있고(구성,composition) 구체적인 행동을 몰라도 되는 것이다.

posted by 여성게
:
일상&기타/책 2019. 8. 11. 22:40

지금까지 읽은 책 기록남기기 시작.

posted by 여성게
:
일상&기타/책 2019. 8. 11. 22:39

posted by 여성게
:
일상&기타/책 2019. 8. 11. 22:38

posted by 여성게
:
일상&기타/책 2019. 8. 11. 22:35

posted by 여성게
: