노무현 대통령 배너

Observer Pattern 옵저버 패턴 – 이벤트 디스패처를 이용해 구현

by on 6.08, 2009, under Design Pattern

bubble지난 옵저버 패턴커스텀 이벤트에 대한 포스트에서 AS3.0 의 이벤트 시스템이 옵저버 패턴과 밀접한 관련이 있다고 하였는데요, 이번에는 trace 만 찍지 않고, 실제 액션스크립트로 비주얼 하게 구현을 해 보도록 하겠습니다.

요건은 다음과 같습니다.

  • AS3.0 의 이벤트 디스패처를 이용할 것.
  • 화면에 그래픽 객체가 각자 움직이다가 마우스 클릭을 하면 한 지점으로 모인다.
  • 다시 한번 클릭하면 그래픽 객체들이 다시 각자 움직인다.

옵저버 패턴이므로 등장인물 3인이 필요하겠습니다.

  1. 주제 객체
    이벤트 디스패처 (EventDispatcher) 가 주제 역할을 하게 됩니다. 이벤트 시스템의 중심에서 이벤트를 전파하는 역할을 하고 있죠.
  2. 옵저버 객체
    여기서는 화면에서 움직이는 그래픽 객체가 옵저버 입니다. 옵저버들은 평소에는 다른 기능을 하다가[01] 이벤트가 발생하면 지정된 곳으로 모이게 됩니다.
  3. 인포 객체
    이벤트 디스패처가 옵저버 들에게 전달하는 Event 내부에 같이 실어 보내는 데이터가 인포 객체 입니다.

화면과 기능을 어떻게 구현할 것인가 좀더 구체화 시킬 필요가 있을 것 같습니다.

  • 옵저버 객체들이 움직이는 방식으로는 Timer 를 이용해 2초 주기의 TweenMax 의 베지어 트윈을 이용해 랜덤한 위치로 결정.
  • 주제가 옵저버에게 전달하는 정보로는 MouseEvent.MOUSE_DOWN 이 일어난 mouseX 와 mouseY 좌표값으로하여 그 정보에 따라 옵저버 객체들이 모이도록 한다.
  • 옵저버 객체들이 주제에 등록되고 등록해제 되는 것을 옵저버 객체에 대한 MouseEvent.MOUSE_DOWN 으로 구현한다.
  • 최초 도큐먼트 클래스에서 모두 옵저버 등록을 하고, 옵저버 객체에 MouseEvent.MOUSE_DOWN 이 한번 일어나면 옵저버 리스트에서 빠지고 (동시에 시각적으로 차이를 내기 위해 alpha = 0.5 해줌) 다시한번 MouseEvent.MOUSE_DOWN 이 일어나면 다시 옵저버 리스트로 들어가도록 한다.

그 외에 옵저버와 직접적인 기능과는 관계 없는 내용으로, MouseEvent.ROLL_OVER 가 일어난 객체를 가장 상위로 올리도록 한 후 상하 관계를 쉽게 알 수 있도록 옵저버 객체 중앙에 십자 표시를 하였고, 주제 객체가 정보를 새로 생성했는지 아닌지 시각적으로 구분하기 위해 정보가 생성되면[02] 배경 이미지의 색상을 바꾸는 내용이 추가로 들어갔습니다.

그럼 이제 주제 인터페이스를 보도록 하겠습니다.

0
1
2
3
4
5
6
7
8
package 
{
	public interface ISubject
	{
		function addObserver( $o:IObserver ):void
		function removeObserver( $o:IObserver ):void
		function notifyObserver():void
	}
}

지난번 옵저버 패턴과 완전히 동일합니다.

주제 구상 클래스는 정보의 전달을 이벤트 디스패처가 담당하게 되었으므로 약간 다릅니다.

0
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
package 
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
 
	public class Subject implements ISubject 
	{
		private var dispatcher:EventDispatcher = new EventDispatcher();
		private var _arrObserver:Array = new Array();
		private var _infoObject:Object;
 
		public function Subject()
		{
 
		}
 
		//ISubject 를 구현 - 옵저버 등록
		public function addObserver( $o:IObserver ):void
		{
			dispatcher.addEventListener( ObserverEvent.NOTIFY, $o.update );
			addObject( $o );
		}
 
		//옵저버에 등록된 대상 객체를 배열에 저장
		private function addObject( $o:IObserver ):void
		{
			arrObserver.push( $o );
		}
 
		//ISubject 를 구현 - 옵저버 제거
		public function removeObserver( $o:IObserver ):void
		{
			dispatcher.removeEventListener( ObserverEvent.NOTIFY, $o.update );
			removeObject( $o );
		}
 
		//옵저버를 제거하면 배열에서 제거
		private function removeObject( $o:IObserver ):void
		{
			var index:int = arrObserver.indexOf( $o, 0 )
			arrObserver.splice( index, 1 )
		}
 
		//ISubject 를 구현 - 이벤트 발생 --> 정보 전달
		public function notifyObserver( ):void
		{
			dispatcher.dispatchEvent( new ObserverEvent( ObserverEvent.NOTIFY, this.infoObject ) )
		}
 
		//도큐먼트 클래스에서 호출할 메서드
		public function setClick( $x:int, $y:int ):void
		{
			makeInfoObject( $x, $y )
			notifyObserver();
		}
 
		//정보를 Object 로 만들어 변수에 저장
		private function makeInfoObject( $x:int, $y:int ):Object
		{
			return _infoObject = { infoX : $x, infoY : $y }
		}
 
		public function get infoObject():Object { return _infoObject; }
 
		public function get arrObserver():Array { return _arrObserver; }
	}
}

EventDispatcher 객체를 생성했고 구상 객체 목록을 관리할 수 있도록 배열을 하나 만들었습니다.
이벤트를 통해 전달할 정보를 저장할 Object 객체도 하나 만들었네요.
ObserverEvent.NOTIFY 가 발생하면 dispatcher가 각각의 옵저버들이 걸어놓은 $o.update 메서드를 실행 하는 것입니다.

디스패처용 커스텀 이벤트 입니다. 커스텀 이벤트에 대한 포스트에서도 있듯이 커스텀 이벤트의 형태는 변수명이나 상수명을 제외하고는 다른 커스텀 이벤트와 동일합니다.

0
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
package 
{
	import flash.events.Event;
 
	public class ObserverEvent extends Event
	{
		static public const NOTIFY:String = "notify";
		private var _info:Object;
 
		public function ObserverEvent( $type:String, $info:Object, $bubble:Boolean = false, $cancelable:Boolean = false )
		{
			super( $type, $bubble, $cancelable );
			this._info = $info;
		}
 
		override public function clone():Event
		{
			return new CustomEvent( type, _info, bubbles, cancelable );
		}
 
		public function get info():Object
		{
			return _info;
		}
	}
}

옵저버 쪽으로 넘어가 보겠습니다. 인터페이스에는 dispatcher가 호출할 update() 메서드 이외엔 없군요.

0
1
2
3
4
5
6
7
package 
{
	import flash.events.Event;
	public interface IObserver
	{
		function update( $e: Event ):void
	}
}

위의 인터페이스를 구상하는 옵저버 구상 클래스 입니다.

0
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package 
{
	import flash.display.Shape;
	import flash.display.Sprite
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.utils.Timer;
	import gs.TweenMax
	import gs.easing.*;
 
	public class Item extends Sprite implements IObserver
	{
		public var txtId:TextField;
		private var _id:uint;
		private var _x:int;
		private var _y:int;
		private var stageWidth:uint;
		private var stageHeight:uint;
		private var timer:Timer = new Timer( 2000, 0 )
 
		private var isInfo:Boolean;
 
		public function Item ( $size:uint = 50 ) 
		{
			var circle:Shape = new Shape()
			circle.graphics.lineStyle( 0, 0, 0 )
			circle.graphics.beginFill( 0x000000, 1 )
			circle.graphics.drawCircle( 0, 0, $size )
			circle.graphics.endFill()
			addChild( circle )
 
			var hLine:Shape = new Shape();
			hLine.graphics.lineStyle( 1, 0xcc3300 );
			hLine.graphics.lineTo( 60, 0 );
			hLine.x = -30;
			addChild( hLine )
 
			var vLine:Shape = new Shape();
			vLine.graphics.lineStyle( 1, 0xcc3300 );
			vLine.graphics.lineTo( 0, 60 );
			vLine.y = -30;
			addChild( vLine )
 
			txtId = new TextField()
			var format:TextFormat = new TextFormat()
			format.font = "Verdana";
			format.color = 0xFFFFFF;
			format.size = 20;
			txtId.defaultTextFormat = format
			txtId.selectable = false;
			txtId.autoSize = "left";
			txtId.backgroundColor = 0x000000;
			addChild(txtId)
 
			this.addEventListener( Event.ADDED_TO_STAGE, initStage );
 
			timer.addEventListener( TimerEvent.TIMER, onTimeHandler )
			timer.start()
		}
 
		//옵저버가 되기위한 구현
		public function update( $e:Event ):void
		{
			var event:ObserverEvent = $e as ObserverEvent;
			this._x = event.info.infoX;
			this._y = event.info.infoY;
			// 정보로 들어온 값이 undefined 인지 아닌지 확인하여 isInfo에 Boolean 으로 저장
			this.isInfo = ( _x && _y ) ? true : false;
		}
 
		private function onTimeHandler( e:TimerEvent = null ):void 
		{
			if ( isInfo )
			{
				concentration()
			}
			else
			{
				deconcentration()
			}
		}
 
		private function initStage( e:Event ):void 
		{
			removeEventListener( Event.ADDED_TO_STAGE, initStage );
			stageWidth = stage.stageWidth;
			stageHeight = stage.stageHeight;
		}
 
		public function set id ( $value:uint ):void 
		{
			_id = $value;
			txtId.text = _id.toString()
		}
 
		public function get id():uint { return _id; }
 
		//Event 의 정보 값으로 모이기
		public function concentration():void
		{
			TweenMax.to( this, 1, { x:_x, y:_y , ease:Quart.easeInOut } );
		}
 
		//Event 정보 값이 undefined 일 경우 랜덤으로 움직이기
		public function deconcentration():void
		{
			TweenMax.to( this, 2, { x:Math.round(Math.random() * stageWidth), y:Math.round(Math.random() * stageHeight), 
					bezier:[ { x: Math.round(Math.random() * stageWidth), y: Math.round(Math.random() * stageHeight) } ], 
					ease:Quart.easeInOut } );
		}
	}
}

구상 클래스의 길이가 약간 길지만 대부분 생성자에서 Shape로 그림을 그리는 부분과, 객체 본연의 할일을 부분[03] 들이고, 옵저버로서의 내용은 인터페이스 메서드인 update() 메서드를 구상하는 내용 뿐 입니다.

그럼 호스트코드(도큐면트 클래스) 에서는 어떻게 주제로 하여금 옵저버들에게 정보를 주게 할까요?

0
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
87
88
89
90
91
92
93
94
95
96
97
98
package 
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import gs.TweenLite;
 
	public class Main extends Sprite 
	{
		private var subject:Subject;
		private var isInfo:Boolean = true;
		private var clickedItem:Array = new Array();
		private var bg:Sprite;
 
		public function Main():void
		{
			subject = new Subject()
			createBG();
			createItem();
		}
 
		private function createBG():void
		{
			bg = new Sprite()
			bg.graphics.lineStyle(0, 0, 0)
			bg.graphics.beginFill(0x555555, 1)
			bg.graphics.drawRoundRect( 0, 0, stage.stageWidth, stage.stageHeight, 50, 50 )
			bg.graphics.endFill()
			addChild( bg )
			bg.addEventListener( MouseEvent.MOUSE_DOWN, bgClickHandler)
		}
 
		private function createItem():void
		{
			for ( var i:uint = 0; i < 10; i++ )
			{
				var item:Item = new Item()
				item.id = i;
				item.buttonMode = true;
				item.addEventListener( MouseEvent.MOUSE_DOWN, itemClickHandler );
				item.addEventListener( MouseEvent.MOUSE_OVER, itemRollOverHandler );
				addChild( item );
 
				subject.addObserver( item );
			}
		}
 
		//옵저버 객체에 마우스 이벤트가 일어나면 handleItem() 으로 객체를 보냄
		private function itemClickHandler( e:MouseEvent ):void 
		{
			var target:Item = e.currentTarget as Item
			handleItem( target )
		}
 
		//주제에 있는 옵저버 리스트(배열)을 조회하여 리스트에 있으면 옵저버 등록을 해지 (없으면 옵저버 등록)
		private function handleItem( $item:Item ):void
		{
			var isFind:Boolean;
			for each( var item:Object in subject.arrObserver )
			{
				if ( item === $item )
				{
					subject.removeObserver( $item );
					$item.alpha = 0.5;
					isFind = true;
					break
				}
			}
			if ( !isFind )
			{
				subject.addObserver( $item )
				$item.alpha = 1;
			}
		}
 
		//롤오버된 객체는 가장 위로 올림
		private function itemRollOverHandler( e:MouseEvent ):void 
		{
			setChildIndex( e.currentTarget as Sprite, numChildren - 1 );
		}
 
		//배경을 클릭한 지점을 주제 메서드로 보냄
		private function bgClickHandler( e:MouseEvent ):void
		{
			isInfo = !isInfo;
 
			if ( isInfo )
			{
				subject.setClick( undefined, undefined );
				TweenLite.to( bg, 1, { tint:0x555555 } );
			}
			else
			{
				subject.setClick( mouseX, mouseY );
				TweenLite.to( bg, 1, { tint:0x8D2545 } );
			}
		}
	}
}

각 메서드에 주석을 달아 놓았으니 참고하시면 되겠습니다.
위의 도큐먼트 클래스를 컴파일한 결과입니다.

배경을 한번 클릭하면 이벤트가 발생하고, 다시한번 클릭하면 객체들은 다시 원래의 할일을 합니다.
객체를 클릭하면 옵저버 리스트에서 빠지고, 다시한번 클릭하면 옵저버에 등록됩니다.

이벤트 모델을 이용한 옵저버 패턴은, 옵저버 객체를 임의로 빼거나 추가해도 디스패처로가 다른 옵저버들에게 전달하는 이벤트에는 아무런 문제가 발생하지 않게되죠.[04]

이렇게 디스패처에 의해 발생된 이벤트에 정보를 실어 옵저버 객체에 전달하고 이벤트 리스너에 등록된 함수를 이용해 객체 내부를 업데이트 하여 정보를 사용하는 방법까지 표현하였습니다.

Observer Pattern Dispatcher Event Example 액션스크립트 코드 다운로드 (184)
이 글을 복사해서 퍼가시는건 허용하지 않습니다. 글의 주소를 다른곳에 알려주시는 것은 환영합니다.
  1. 화면에서 각자 움직이다가 []
  2. 즉, 배경에 MouseEvent.MOUSE_DOWN 이 일어나면 []
  3. 랜덤으로 2초마다 움직이게 하는 일 []
  4. 객체간 약한 결합 : Loose Coupling []

관련된 글

:, , , , , , , , , , , , , ,

6 Comments for this entry

  • 흠흠No Gravatar

    버그가 있네요 몇몇 객체들은 이벤트를 받지 않는군요

    • 세계의끝No Gravatar

      그러니까 객체에 클릭을 하면 옵저버 리스트에 들락날락하는 toggle 을 하게 되죠.
      이 내용은 본문에도 써 놓았습니다.
      addObserver(), removeObserver() 메서드 만들었으면 이용해 봐야죠.
      이걸 버그라 하시면 난감하네요.

  • 폐인No Gravatar

    잘 보고 갑니다..^^

  • 미친바람No Gravatar

    흐흐흐. 이걸 공부하려구 가지고 있는 액션스크립트 책 5권을 모두 뒤져서 이벤트에 대해 공부를 했슴다. 이제부터 이걸 씹어먹어 보렵니다. 다시 한번 감사드립니다. 완전 생초보에게 이런 성찬을 차려주셔서요. 그리고 플생사모 까페에 제가 공부한 내용을 다시 올립니다. 여긴 올리는 기능이 없어서요. ^^. 먼 미친바람이 부는 베트남에서…

    • 세계의끝No Gravatar

      본문은 이벤트로 통신을 하고 있지만, 옵저버 패턴이 반드시 이벤트 디스패치를 이용해야 하는 것은 아니라는것을 생각하실 필요가 있습니다.
      멀리 계시는군요. 열심히 공부하세요 ^^

1 Trackback or Pingback for this entry

Leave a Reply

Looking for something?

Use the form below to search the site:

Still not finding what you're looking for? Drop a comment on a post or contact us so we can take care of it!

Meta