컴퓨터/정보처리기사 SW 공학

| 니앙팽이 - 객체지향(OOP) | 4-6 | 행동패턴 - Command pattern

객체지향

📕 4. 객체지향 디자인 패턴

유니티에서 사용하면 좋을 디자인 패턴만 명시한다.


📄 5. Command Pattern 행동 패턴


1. 커맨드 패턴의 특징

  1. 함수 호출(요청)을 저장하는 객체 형태로 캡슐화

    • Excute와 Undo를 캡슐화 하여 관리하는데 유용한 패턴이다.
    • Command라는 추상적인 단위로 ConcreteCommand라는 클래스를 만들 수 있고.
    • 함수호출이란, 리시버에 의존하여 리시버의 함수를 수행한다 라는것.
  2. 함수 호출 타이밍 조절 & 로깅

    1. 커맨드를 콜렉션(Queue, Stack, List)으로 관리할 수 있다.
      실행 타이밍에 순서를 둘 수 있으니 턴제 게임에도 유용하다.
  3. 굳이 그 클래스에 정의되지는 않지만.

    • 특정한 로직을 수행하는 메소드를 외부에 정의하도록 하여
    • 함수에 사용될 데이터와 객체만 있다면 새로운 메소드를 만들고 다른곳에서 사용 가능

2. 왜 사용하는가

① 함수 호출(요청을) 캡슐화 하기 위해

  • 1. 책임 분배로, 작업을 여러개로 분리
    2. 결합도를 낮춘다. 
    3. 재사용성이 높은 구조로 사용하는 패턴이다.
    4. 커맨드 객체를 콜백으로 사용하는 
    

② 명령의 실행의 히스토리를 관리한다.
③ 명령의 비동기 처리 및 큐

④ 공통점과 차이점

  • 델리게이트와 커맨트 패턴의 공통점은.
        콜백 매커니즘 사용할때 델리게이트, 커맨드패턴 둘다 사용 가능하다.
        메소드를 객체화 하여, 컨테이너로 관리할 수 있는 형태
    
    차이점은 : 델리게이트
        델리게이트는 함수 포인터를 참조하는 타입으로 메소드 참조를 저장하고 호출 할 수 있다.
        이벤트 매커니즘을 구현하는데 사용 가능하다. 이건 옵저버 패턴과도 일맥 상통하는 부분이다.
            한 객체의 상태가 변경될때, 이를 의존하는 다수의 객체들에게 알리고자 할때 사용가능하다.
        비동기 프로그래밍
    
    차이점은 : 커맨드 패턴은
        Receiver라는 의존하는 객체를 참조하여 그 객체가 가지고 있는 메소드를 실행하는 매커니즘이다.
        따라서 클라이언트, 커맨드, 리시버간 의존성이 있다는것이다.
    

⑤ 사용 예시 EX )

  • 1. 턴제 게임
        * 함수 호출 커맨드를 컨테이너로 관리하는 Invoker 사용
    2. 함수 호출 Logger
        * Undo/Redo
        * ReplaySystem
    3. 컨트롤러 키보드 매핑
    

3. 커맨드 패턴 구성요소

  • Client

    • ConcreteCommand의 객체 생성을 담당.
      그리고 그 명령을 수행하는 Receiver를 참조하는 관계이다.
  • Command :

    • 적어도 ConcreteCommand면 가져야할 메소드를 것을 정의한 인터페이스 || 추상클래스
      1. Execute를 수행
      2. Undo/Redo를 수행
  • ConcreteCommand

    • Command를 구현한 객체
    • Receiver를 참조하여 메소드를 의존하여 특수화 된 객체들이다.
    • 즉 이 구체 커맨드의 목적은 자기 데이터를 가지는 일 없고, 오직 외부 Reciever에 특수한 동작을 시키는데 구성된 녀석이다.
    • 클래스 네이밍은 행동에대한 동사형 적기 (Ex. TurnTvOn, JumpCommand)
  • Receiver :

    • Command가 의존하는, 혹은 의존 주입이 되는 객체
  • Caller || Invoker :

    • Replay 를 수행
    • Client클래스에서 생성한 ConcreteCommand 객체들을
      1. 저장하고, 2. 실행의 책임이 있다.
    • 선택적으로 커맨드객체를 컬렉션(Queue, Stack, List)로 보관하고,
명령 패턴을 쓰다보면 수많은 Command 클래스를 만들어야 할 수 있다. 
이럴 때에는 상위 클래스에 여려 가지 편의를 제공하는 상위 레벨 메서드를 만들어 놓은 뒤에 
필요하면 하위 클래스에서 원하는 작동을 재정의할 수 있게 하면 좋다. 
이러면 명령 클래스의 execute 메서드가 하위 클래스 샌드박스 패턴으로 발전하게 된다.

4. 커맨드 패턴의 예시

레스토랑에 온 소님을 예시로 들어보자

"손님"(Client)"웨이터"(Invoker) 에게
"음식의 제작(FoodOrderConcreteCommand)""말/요청(Command)" 했다.
"음식을 만드는 쉐프(Receiver)" 는 주문에 따라 음식을 만든다.

ⓓ 예시

📂IHeartGameDev Command (C#)📂
/*ICommnad.cs*/
public interface ICommand {
    void Execute();
    void Redo();
    void Redo();
}

/*ConcreteCommands.cs*/
public class TurnOnCommnad() : ICommand {
    LightBulb mLbRef;
    //Previous
    public TurnOnCommnad(LightBulb lb) { mLbRef = lb; }
    public Execute() => mLbRef.TurnOn();
}

public class TurnOffCommnad() : ICommand {
    LightBulb mLbRef;
    //Previous
    public TurnOffCommnad(LightBulb lb) { mLbRef = lb; }
    public Execute() => mLbRef.TurnOff();
}

/*LightBulb.cs*/
public class LightBulb : MonoBehaviour {
    public void TurnOn();
    public void TurnOff();
}

/*Client.cs*/
public class Client : MonoBehaviour{
    public LightBulb lb;
    void Update() {
        if(Input.GetKeyDown(KeyCode.Space)) {
            ICommand storedCommand = new TurnOnCommand(lb);
            storedCommand.Execute();
        }
    }
}

public class LightSwitch {
    ICommand _onCommand, _offCommand;

    public void AddCommand();
    public void PowerOn()   {   _onCommand.Execute();   }
    public void PowerOff()  {   _onCommand.Execute();   }
}
📂Undo Command (C#)📂
namespace DesignPattern
{
    //Base class for the commands
    //This class should always look like this to make it more general, so no constructors, parameters, etc!!!
    public abstract class Command
    {
        public abstract void Execute();
        public abstract void Undo();
    }   
}
📂Command Pattern 정석적인 예시 (C#)📂
namespace DesignPattern
{
    namespace Device
    {
        public interface ElectronicDeviceReceiver
        {
            public void On();
            public void Off();
            public void VolumeUp();
            public void VolumeDown();
        }

        //리시버
        public class Television : ElectronicDeviceReceiver {
            private int volume = 0;
            public void On() => Console.WriteLine("TV is On");
            public void Off() => Console.WriteLine("TV is Off");
            public void VolumeUp() => this.volume++;
            public void VolumeDown() => this.volume--;

        }

        //리시버
        public class Radio : ElectronicDeviceReceiver {
            private int volume = 0;
            public void On() => Console.WriteLine("Radio is On");
            public void Off() => Console.WriteLine("Radio is Off");
            public void VolumeUp() => this.volume++;
            public void VolumeDown() => this.volume--;
        }
    }

    namespace Command
    {
        public interface Command {
            public void Execute();
        }

        //콘크리트 커맨드
        public class TurnTvOn : Command {
            private ElectronicDevice electronicDevice;
            public TurnTvOn(ElectronicDevice electronicDevice){
                this.electronicDevice = electronicDevice;
            }

            public void Execute() => electronicDevice.On();
        } 

        //콘크리트 커맨드
        public class TurnTvOff : Command {
            private ElectronicDevice electronicDevice;
            public TurnTvOff(ElectronicDevice electronicDevice){ this.electronicDevice = electronicDevice; }

            public void Execute() => electronicDevice.Off();
        }

        public class TurnAllInvoker : Command {
            List<ElectronicDevice> electroDevices;
            public TurnAllInvoker() { this.electroDevices = new List<ElectronicDevice>(); }
            public TurnAllInvoker AddDevices(ElectronicDevice electronicDevice){
                this.electroDevices.Add(electronicDevice);
                return TurnAllInvoker;
            }

            public void Execute() =>  foreach(ElectronicDevice E in this.electroDevices){E.Off();}
        }

        public class DeviceButtonInvoker{
            Command theCommand;
            public DeviceButton(Command command){ this.theCommand = command; }
            public void Press() => theCommand.Execute();
        }
    }

    public class Client() {
        ElectronicDevice tv = new Television();

        TurnTvOn onCommand = new TurnTvOn(tv);
        TurnTvOff offCommand = new TurnTvOff(tv);

        DeviceButton onButton= new DeviceButton(onCommand);

        onButton.Press();
        onButton = new DeviceButton(offCommand);
        onButton.Press();

        ElectronicDevice radio = new Radio();

        TurnAllDevices allOffCommand= new TurnAllDevices();
        allOffCommand.AddDevices(radio).AddDevices(tv);

        onButton = new DeviceButton(allOffCommand);
        onButton.Press();
    }
} 
📂Command Pattern 키 변경 (C#)📂
using System.Collections;
using UnitEngine;

namespace DesignPattern
{
    public interface Command {
        public void Execute(GameObject obj);
    }

    public class ConcreteCommandAttack : Command {
        public void Execute(Gameobject obj) { Attack(obj); }
        void Attack(Gameobject obj){
            obj.transform.Translate(Vector3.forword);
        }
    }
    public class ConcreteCommandDefense : Command {
        public void Execute(GameObject obj){ Defense(obj);	 }

        void Defense(GameObject obj)
        {
            Debug.Log(obj.name + " Defense");
            obj.transform.Translate (Vector3.back);
        }
    }

    public class PlayerKeyClient : MonoBehaviour {
        CommandKey btnA, btnB;

        void Start () { SetCommand(); }

        void SetCommand()
        {
            btnA = new CommandAttack();
            btnB = new CommandDefense();
        }

        public void BtnCommandA() => btnA.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 공격하게 함
        public void BtnCommandB() =>  btnB.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 방어하게 함
    }
} 

참조

  1. IHeartGameDev : 기본적인 Command Pattern 입문
  2. git-amend : 비동기 큐와 조합한 CommandQueue
  3. Infallible Code : Undo / Redo Command 예시
  4. 코드없는 프로그래밍 : 커맨드 패턴
  5. Derek Banas : Java 예시
  6. 식빵망 : 명령 패턴