요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.
- 위키백과 -
이번에 알아볼 건 커맨드 패턴이다.
커맨드 패턴은 지금까지 해온 패턴과는 달리 어느 정도 룰과 구성요소가 정해져 있다.
- 리시버 (Receiver) : 기능을 수행할 대상
- 커맨드 (Command) : 리시버를 제어하는 객체
- 인보커 (Invoker) : 커맨드들을 제어하는 객체
- 클라이언트 (Client) : 인보커를 제어하는 객체
위와 같이 총 4가지로 구성되어 커맨드 패턴을 구현하게 된다.
먼저 리시버부터 알아보자.
// 리시버
public class Skill
{
public void BladeDance() { Console.WriteLine("블레이드 댄스!"); }
public void CriticalBuff() { Console.WriteLine("크리티컬 버프!"); }
}
여기서는 Skill 이라는 객체를 리시버로 사용하겠다.
이 객체에는 BladeDance 라는 액티브 스킬과 CriticalBuff 라는 버프 스킬이 존재한다.
(필요에 따라서는 인터페이스나 추상 클래스로 구현에 유연성을 주는 것이 좋을 것이다.)
다음으로는 리시버를 제어하는 커맨드 객체들이다.
public interface Command
{
void Execute();
}
public class SCBladeDance : Command
{
private Skill skill;
public SCBladeDance(Skill skill) => this.skill = skill;
public void Execute() => skill.BladeDance();
}
public class SCCriticalBuff : Command
{
private Skill skill;
public SCCriticalBuff(Skill skill) => this.skill = skill;
public void Execute() => skill.CriticalBuff();
}
Command 인터페이스를 통해 SCBladeDance, SCCriticalBuff 클래스를 구현하였다.
각 객체에서는 Skill 클래스를 생성자에서 매개 변수로 받아 멤버 변수로 저장하고
적절한 함수를 Execute 에서 호출하고 있다.
그렇다면 여기서 굳이 왜 리시버를 커맨드 객체에 다시 담는지 의문이 들 수 있다.
전략 패턴 (Strategy Pattern) 에서 처럼 리시버를 캡슐화하여 바로 사용해도 되지 않을까?
이 패턴의 정의를 다시 한번 살펴보면 나와있듯
객체의 확장성에 대한 유연함을 주기 위한 것이라고 할 수 있다.
만약 스킬이 특정한 패턴(보스나 전투 중)에 의해서 강제로 취소되는 걸 구현해야 한다고 했을 때
Command 인터페이스에 Cancel이라는 메소드를 추가하고 이에 따른 추가적인 구현만 해주면 된다.
물론 이 또한 리시버에 바로 넣어서 구현할 수 있지만
만약 커맨드 객체가 서로 다른 객체를 여러 개 가지고 있다면 이해가 좀 더 쉽게 될 것이다.
(특정 스킬에 효과를 부여하는 아이템 등을 여기서 같이 관리한다거나...)
이번에는 인보커에 대해서 알아보자.
// 인보커
public class SkillSlot
{
private Command[] slots;
public SkillSlot(int slotSize) => slots = new Command[slotSize];
public void UseSkill(int slot) => slots[slot]?.Execute();
public void SetSkill(int slot, Command skill) => slots[slot] = skill;
}
SkillSlot 에 Command 인터페이스 배열을 만들어 슬롯을 구현했다.
초기화 시 배열의 크기를 지정하고 SetSkill 메소드를 통해 슬롯별 스킬을 지정할 수 있다.
여기서는 단순히 각 커맨드를 지정하고 실행하는 기능을 수행한다.
var skillSlot = new SkillSlot(2);
var bladeDance = new SCBladeDance(new Skill());
var critBuff = new SCCriticalBuff(new Skill());
skillSlot.SetSkill(0, bladeDance);
skillSlot.SetSkill(1, critBuff);
skillSlot.UseSkill(1);
skillSlot.UseSkill(0);
skillSlot.UseSkill(0);
그리고 이 코드가 클라이언트 부분에 해당한다고 볼 수 있다.
패턴을 정리하여 마치도록 하겠다.
- 목적 : 요청을 객체로 캡슐화하고 재활용성과 확장의 유연함을 준다.
- 방법 : 리시버, 커맨드, 인보커, 클라이언트 4가지 요소를 통해 기능을 분할하여 구현한다.
커맨드 객체의 의의에서 보았듯이 기능을 분할하고 유연함을 주는 게 객체지향에 있어서 좋은 건 맞지만
때로는 이러한 구조가 개발 중인 환경에 따라서는 오히려 복잡하게 돼버릴 수 있다.
디자인 패턴은 어디까지나 어떤 문제를 해결하기 위한 검증된 해결책이지
~하지 않으면 절대 안 돼 라는 등으로 생각하는 건 잘못된 생각이지 않을까 생각한다.
실제 개발에서 객체지향의 원칙들을 전부 지키며 좋은 패턴을 전부 적용시키는건 불가능에 가깝다.
'디자인 패턴' 카테고리의 다른 글
7. 빌더 패턴 (Builder Pattern) (0) | 2021.01.09 |
---|---|
6. 옵저버 패턴 (Observer Pattern) (0) | 2020.12.26 |
4. 데코레이터 패턴 (Decorator pattern) (0) | 2020.12.21 |
3. 싱글턴 패턴 (Singleton pattern) (0) | 2020.12.19 |
2. 팩토리 패턴 (Factory Pattern) (2) | 2020.12.18 |