옵저버 패턴은 실제 개발할 때 많이 쓰이는 패턴 중에 하나이다. 

 

옵저버 패턴은 하나의 서브젝트(subject)와 다수의 옵저버(observer)들 사이의 상호작용을 좀 더 결합도는 낮게, 응집도는 높게 만드려는 노력의 결과물이다.

서브젝트에서 어떤 일이 일어난다면, 다수의 옵저버들에게 그 일이 발생했다는 것을 알려주고, 옵저버들은 그 일에 반응하여 모종의 행동을 하게 되는 구조이다.

 

게임을 하나 예로 들어보자. 어떤 미로형 탈출 게임에서 맵 중앙에 있는 한 개의 스위치를 누르면 방의 여러개 있는 문들이 열리거나 닫힌다고 해보자. 어떤 문은 열리고, 어떤 문은 닫히고, 어떤 문은 특정 조건일 때만 열리거나 닫힌다. 갑자기 괴물이 튀어나와서 특정 방에 유인해서 가둬야 하기도 하는 등 스위치 컨트롤이 핵심인 게임이라고 해두자. 여기서 중요한 것은 스위치를 누르는 것은 한 번이지만, 문 별로 반응하는 것이 다르다는 게 핵심이다.

 

옵저버 패턴에서 중요한 점은 open/closed principle(OCP)이다. 문의 종류나 갯수를 추가하더라도 스위치버튼 클래스의 내부를 수정하지 않아도 되게 하는 점이 바로 포인트이다. 

 

일단 문의 종류를 정해보자.

 

연한 남색 : 스위치를 한 번 누르면 열리거나 닫히는 일반문.

보라색 : 스위치를 두 번 눌러야 열리거나 닫히는 무거운 문.

연두색 : 연두색 열쇠를 얻어야 스위치에 반응하는 문.

노란색 : 노란색 열쇠를 얻어야 스위치에 반응하는 문.

진한 남색 : 보스를 쓰러뜨려야 열리는 다음 챕터로 가는 문.

 


 

 

만약 옵저버 패턴을 쓰지 않는다면, 아래와 같이 해야할 것이다(물론 일부러 노골적으로 불편하게 만들어보았다).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchButton : MonoBehaviour
{
    public SkyBlueDoor skyblue1;
    public SkyBlueDoor skyblue2;

    public VioletDoor violet1;
    public VioletDoor violet2;

    public GreenDoor green1;

    public YellowDoor yellow1;

    public BlueDoor blue1;

    //스위치 눌렀을 때
    public void OnClickSwitch()
    {
        skyblue1.OepnOrClose();
        skyblue2.OepnOrClose();

        violet1.OepnOrClose();
        violet2.OepnOrClose();

        green1.OepnOrClose();

        yellow1.OepnOrClose();
        
        blue1.OepnOrClose();
    }
}

 

 

이렇게 되면 작동은 할 것이다. 하지만 굉장히 비효율 적이다. 왜냐하면 문을 더 만들거나 종류가 늘어났을 때마다 서브젝트(여기서는 SwitchButton 클래스)를 항상 수정해야 하기 때문이다. 이것은 객체지향이 원하는 바가 아니다.

 

그래서 최종적으로는 아래와 같이 수정해주도록 한다.

 

1. 문들은 Door라는 추상클래스로 묶어준다.

2. SwitchButton 클래스는 Door를 배열이나 리스트로 관리해주며, 문이 늘어나던 말던 SwitchButton 클래스는 신경 안쓴다.

3. 버튼이 눌리면 그 눌렸다는 사실을 각 문들에게 인지시켜주고, 문들은 그에 따라 열리거나 닫힌다.

 

 

 

우선 스위치 클래스는 아래와 같다.

public class SwitchButton : MonoBehaviour
{
    public List<Door> doors = new List<Door>();

    public void AddDoor(Door door)
    {
        if (doors.Contains(door))
        {
            Debug.Log("이미 해당 문이 리스트에 존재하고 있음.");
        }
        else
        {
            doors.Add(door);
        }
    }

    public void RemoveDoor(Door door)
    {
        if(doors.Contains(door))
        {
            doors.Remove(door);
        }
        else
        {
            Debug.Log("문이 리스트에 존재하지 않아서 제거하지 못함.");
        }
    }

    public void OnClickSwitchButton()
    {
        NotifyDoors();
    }

    public void NotifyDoors()
    {
        foreach(Door d in doors)
        {
            d.OpenOrClose();
        }
    }
}

 

 

아래는 Door라는 추상클래스이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class Door : MonoBehaviour
{
    public abstract void OpenOrClose();
}

 

 

그리고 Door를 상속받아서 여러가지 문을 제작하면 된다. 여기서는 인벤토리에 노란열쇠가 있으면 열리는 노란문만 구현하도록 하겠다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DoorYellow : Door
{
    bool isClosed;
    public override void OpenOrClose()
    {
        if(Inventory.Instance.HasItem("YellowKey"))
        {
            if(isClosed)
            {
            	isClosed = false;
                Oepn();
            }
            else
            {
            	isClosed = true;
                Close();
            }
        }
    }

    void Open()
    {
		//문 열기 구현
    }

    void Close()
    {
		//문 닫기 구현
    }
}

 

 

이제 문의 갯수가 추가되면 AddDoor 함수를 사용하면 되고, 문의 종류가 늘어나면 추상클래스 Door를 상속받아서 새로 제작하면 된다. OCP도 어기지 않고, 스위치를 누르면 각 문들에게 통지를 해주게 된다.

AddDoor나 RemoveDoor는 문쪽에서 Start함수 같은데에서 직접 할 수도 있고, 아니면 스위치가 하나라는 전제 하에 맵디자인을 관장하는 클래스에서 해줘도 될 것이다.

 

 

한걸음 더 나아가서, C#의 delegate를 사용하는 방법도 있다. delegate를 사용하면 SwitchButton이 Door 객체를 참조하는게 아니고, OpenOrClose()함수를 참조하게 된다. delegate에 대해서는 여기서 다루지 않는다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchButton : MonoBehaviour
{
	//리턴타입이 void이며 매개변수가 없는 함수형을 담을 틀을 만들어준다.
    public delegate void DoorNotification();

	//OpenOrClosed 함수들을 넣어줄 delegate.
    public event DoorNotification doorNotification;

	//매개변수가 DoorNotification 타입이지만 리턴타입이 void이고 매개변수가 없는 함수는 뭐든 받을 수 있다.
    public void AddDoor(DoorNotification notification)
    {
        //중복체크
        foreach (DoorNotification n in doorNotification.GetInvocationList())
        {
            if (n == notification)
            {
                Debug.Log("이미 해당 OpenOrClose 함수가 리스트에 존재하고 있음.");
                return;
            }  
        }

        doorNotification += notification;
    }

    public void RemoveDoor(DoorNotification notification)
    {
        //있는지 체크
        foreach (DoorNotification n in doorNotification.GetInvocationList())
        {
            if (n == notification)
            {
                doorNotification -= notification;
                return;
            }
        }

        Debug.Log("해당 OpenOrClose 함수가 리스트에 존재하지 않아서 제거하지 못함.");
    }

    public void OnClickSwitchButton()
    {
        NotifyDoors();
    }

    public void NotifyDoors()
    {
        //추가되어있던 OpenOrClosed()함수들을 일제히 호출.
        doorNotification.Invoke();
    }
}

+ Recent posts