내일배움캠프 37일차 TIL : 디자인 패턴 3

김정환·2024년 11월 5일
0

키워드

  • 디자인 패턴 3

워낙 중요하고 알아두어야할 것이 많아서 til 3개로 정리하게 됐다.

3. 명령 패턴


명령(행동) 하나하나를 객체로 만든다.

왜 쓸까

  • 주로 특정 행동들을 추적해서 기록을 남기거나(로깅),
    특정 행동들의 사용 내역으로 Undo, Redo 기능이나 히스토리를 위해 사용한다.
  • 행동을 객체로 만들어서 재사용하거나, 기록하거나, 취소, 병렬처리를 쉽게 할 수 있다.
  • 실행된 행동을 하나하나 되돌리면 본래의 값으로 복구할 수도 있다.

명령(행위)의 객체화

  • 일상에서의 예시는 TODO 리스트
  • 행동과 관련된 정보를 객체로 캡슐화하고 이를 관리하는 것.
  • 메서드 -> 커맨드 오브젝트 사용

구현

구현 목적에 따라서 방법이 달라짐.

1. 로깅 시스템 구현 목적

  • 공통된 사용을 위해서 ICommand 인터페이스를 사용.
using System.Collections.Generic;
using UnityEngine;

// 명령 객체를 위한 인터페이스
public interface ICommand
{
    void Execute();
    string GetCommandLog(); // 로깅을 위한 구현
} 

public class BuildCommand : ICommand
{
    GameObject building;
    Vector3 position;

    public BuildCommand(GameObject objectToBuild, Vector3 buildPoint)
    {
        building = objectToBuild;
        position = buildPoint;
    }

    public void Execute()
    {
        // 예시용 : 무리한 사용방법
        GameObject.Instantiate(building, position, Quaternion.identity);
    }

    public string GetCommandLog()
    {
        return $"BuildCommand: {building.name} 생성. 생성 위치 : {position}";
    }
}

public class RemoveCommand : ICommand
{

    GameObject building;
    public RemoveCommand(GameObject objectToBuild)
    {
        building = objectToBuild;
    }

    public void Execute()
    {
        // 예시용 : 무리한 사용방법
        GameObject.Destroy(building);
    }

    public string GetCommandLog()
    {
        return $"RemoveCommand: {building.name} 파괴";
    }
}

public class CommandLogger
{
    List<string> logs = new List<string>();

    public void LogCommand(ICommand command)
    {
        command.Execute();
        logs.Add(command.GetCommandLog());
    }

    void PringLog()
    {
        foreach (var log in logs)
        {
            Debug.Log(log);
        }
    }
}

2. 되돌리기 시스템 구현 목적

  • 메멘토 패턴 적용
    • 객체가 한 행동을 모두 저장하고 있어야 함.
    • 되돌리기의 경우 들어온 순서를 역순으로 꺼내기 위해서 Stack 자료구조를 사용.
  • 파사드 패턴
    • Player 관련 클래스는 많은 클래스들이 서로 상호작용하는 상황.
    • Player 클래스 그룹에 외부의 클래스가 접근해야한다면,
      이미 복잡한 관계의 클래스들과 상호작용하기 어려움.
    • 이를 해결하기 위해서 PlayerFacade를 만들어서 외부 클래스와 소통하는 창구 역할을 만드는 것.
// 명령 객체를 위한 인터페이스
using System.Collections.Generic;
using UnityEngine;

public interface ICommand
{
    Vector3 Execute();
    Vector3 Redo(); // 되돌리기를 위한 구현
}

public class MoveCommand : ICommand
{
    Vector3 _direction;
    float _distance;

    public MoveCommand(Vector3 direction, float distance)
    {
        _direction = direction;
        _distance = distance;
    }

    public Vector3 Execute()
    {
        return _direction * _distance;
    }

    public Vector3 Redo()
    {
        return -_direction * _distance;
    }   
}


// 파사드 패턴
// 내부 로직 구성이 많거나 복잡한 경우
// 외부에서 쉽게 접근할 수 있도록하기 위한 창구 역할
// 주로 api 등등에서 사용하는 방법
public class PlayerFacade : MonoBehaviour
{
    PlayerInput input;
    PlayerMovement movement;

    void Awake() 
    {
        input = GetComponent<PlayerInput>();
        movement = GetComponent<PlayerMovement>();
    }

    void Update()
    {
        // 이동 명령 입력 시, 처리
        Vector3? move = input.ProcessInput();
        if(move.HasValue)
        {
            movement.Move(move.Value);
        }

        // 명령 취소
        if(input.ShouldRewind())
        {
            Vector3? redoMovement = input.Rewind();
            if(redoMovement.HasValue)
            {
                movement.Move(redoMovement.Value);
            }
        }
    }
}

public class PlayerMovement : MonoBehaviour
{
    public void Move(Vector3 movement)
    {
        transform.position += movement;
    }

}

public class PlayerInput : MonoBehaviour
{
    public float moveDistance = 1f;
    Stack<ICommand> history = new Stack<ICommand>();

    public Vector3? ProcessInput()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 dir = new Vector3(h, 0, v);

        if(dir != Vector3.zero)
        {
            // 이동 명령
            ICommand moveCommand = new MoveCommand(dir.normalized, moveDistance);
            history.Push(moveCommand);

            return moveCommand.Execute();
        }

        return null;
    }

    public bool ShouldRewind()
    {
        return Input.GetKeyDown(KeyCode.Escape) && history.Count > 0;
    }

    public Vector3? Rewind()
    {
        if(history.Count > 0)
        {
            ICommand lastCommand = history.Pop();
            return lastCommand.Redo();
        }

        return null;
    }
}

4. 서비스 로케이터

왜 쓸까?

  • 싱글톤(일반적으로 서비스)을 쓰다보면 참조가 난잡해질 수 있음.
    구체적인 서비스에 대한 의존성을 낮추기 위해 사용

특징

  • 서비스에 대한 참조 난잡해지는 것을 막기위해서
    각 로직을 담당하는 코드들이 구체적인 서비스 클래스를 참조하지 않고, 서비스 로케이터를 참조하도록 설계함
  • 이벤트 버스에서와 유사하게 대리자를 참조함.
  • 서비스 로케이터는 이런 서비스들을 등록하고 관리하는 역할을 수행하게 한다.

#내일배움캠프 #스파르타내일배움캠프 #스파르타내일배움캠프TIL

profile
사파 개발자

0개의 댓글