행동패턴 은 지난번 옵서버 패턴을 정리하면서 작성했다 "객체의 통신"에 목적과 의미를 둔 패턴이라고 생각하면 되겠다.
커맨드 패턴이란 ?
요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 메서드 이름, 매게 개변수 등 요청에 필요한 정보를 저장 또는 로깅,취소 할 수 있게 하는 패턴이다. -나무위키-
요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴이다. 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 쉬 초할 수 있는 작업을 지원할 수 있도록 합니다. - 구루-
커맨드라는 추상회된 요청을 이용하는 방식에서 유래되었다. 커맨드 패턴은 요청 자체를 객체로 캡슐화하고, 호출자와 수신자를 분리한다. 이렇게 하면 호출자는 수신자의 인터페이스를 알 필요 없이 커맨드를 수행할 수 있다. 또한, 이런 방식은 다양한 요청, 큐 또는 로그요청, 그리고 가능한 경우에는 요청의 취소도 지원할 수 있다.
지난 패턴들과 동일하게 하나의 인터페이스 추상화를 두고 어떻게 실행하는가에 목적을 두고 있다. 정의에서 보다시피 여러 개의 커맨드를 받아 수행할 수 있다고 하는데 "공통된 인터페이스를 구현하기 때문에 가능하다."
언제 사용해야 하는가?
1. 호출자와 수신자의 분리
- 호출자가 요청을 수행하는 객체나 그 방법에 대해 알 필요가 없는 경우
- 웹사이트의 사용자 요청을 처리하는 시스템 => 각 요청을 커맨드 객체로 캡슐화해서 처리 가능
- 사용자는 실제 요청이 어떻게 처리되는지 알 필요가 없다.
2. 요청 매개변수화
- 요청을 수행하는 방법을 매개변수화 하려는 경우 커맨드 패턴이 유용하다. 요청 선택 또는 사용자 입력 등을 캡슐화하면 호출하는 시점에 서 다른 매개변수를 사용하여 요청을 수행할 수 있다.
- 사용자 인터페이스의 버튼, 각 버튼을 누르면 특정 작업을 수행할 수 있도록 설정
- 커맨드 객체는 특정 작업을 캡슐화하고 있다.
3. 요청 저장 및 로깅
-커맨드 패턴은 요청을 캡슐화하여 저장하고 로깅하는 것을 가능하게 한다.
- DB의 트랜잭션을 예로 들 수 있다. 각 트랜잭션은 수행되어야 할 작업의 목록을 가질 수 있다. 이 목록을 통해 트랜잭션의 성공여부를 확인할 수 있다.
4. 취소 및 되돌리기 기능
- 커맨드 자신의 undo 메서드를 구현해, 실행취소의 작업도 손쉽게 가능하다.
- 텍스트 에디터 의 undo 기능 사용자가 작성한 텍스트는 undo를 통해 원래 작업과 반대작업을 모두 알고 있기 때문에 가능하다.
기본적인 구조
1번의 인보커 발송자는 요청들의 시작점을 나타낸다. 어떠한 커맨드가 설정될지 혹은 어떻게 execute를 할지를 나타내고, 인보커에서 List 형태로 관리되기도 한다.
2번의 커맨드는 구상 커맨드 가 구현해야 할 공통 함수를 명명한다. 보는 바와 같이 통상 1 나의 메서드 만을 표기한다.
3번의 구상 커맨드 들은 excute에서 비즈니스 로직을 수행해 준다.
4번의 수신자는 비즈니스 로직이 포함되어 있으며 "실제 비즈니스 로직 수행을 담당하고 있다"
5번의 클라이언트는 각각의 호출자 수신자 커맨드 등의 인스턴스를 생성하고 요청한다.
코드 예제
package command
import "fmt"
type CommandHead interface {
execute()
undo()
}
type Light struct{}
func (l *Light) on() {
fmt.Println("전구가 켜집니다.")
}
func (l *Light) off() {
fmt.Println("전구가 꺼집니다.")
}
type GarageDoor struct{}
func (g *GarageDoor) up() {
fmt.Println("차고 문 올라가요")
}
func (g *GarageDoor) down() {
fmt.Println("차고 문 내려가요")
}
func (g *GarageDoor) stop() {
fmt.Println("차고 문 멈춥니다.")
}
func (g *GarageDoor) lightOn() {
fmt.Println("차고 에 불이 켜집니다.")
}
func (g *GarageDoor) lightOff() {
fmt.Println("차고 에 불이 꺼집니다.")
}
type GarageOnCommand struct {
g *GarageDoor
}
func (g *GarageOnCommand) execute() {
g.g.lightOn()
g.g.up()
g.g.stop()
}
func (g *GarageOnCommand) undo() {
g.g.lightOff()
g.g.down()
}
func (g *GarageOnCommand) String() string {
return "차고 ON 명령"
}
type GarageDoorOffCommand struct {
g *GarageDoor
}
func (g *GarageDoorOffCommand) execute() {
g.g.lightOff()
g.g.down()
}
func (g *GarageDoorOffCommand) undo() {
g.g.lightOn()
g.g.up()
g.g.stop()
}
type LightOnCommand struct {
light Light
}
func (l *LightOnCommand) execute() {
l.light.on()
}
func (l *LightOnCommand) undo() {
l.light.off()
}
type SimpleController struct {
c CommandHead
}
func (s *SimpleController) SetCommand(c CommandHead) {
s.c = c
}
func (s *SimpleController) ButtonWasPressed() {
s.c.execute()
}
type NoCommand struct{}
func (n *NoCommand) execute() {
fmt.Println("There's no command")
}
func (n *NoCommand) undo() {
fmt.Println("There's no command")
}
type RemoteController struct {
OnCommands []CommandHead
OffCommands []CommandHead
undoCommand CommandHead
}
func (r *RemoteController) SetCommand(slot int, onCmd, offCmd CommandHead) {
r.OnCommands[slot] = onCmd
r.OffCommands[slot] = offCmd
}
func (r *RemoteController) OnButtonWasPushed(slot int) {
r.OnCommands[slot].execute()
r.undoCommand = r.OnCommands[slot]
}
func (r *RemoteController) OffButtonWasPushed(slot int) {
r.OffCommands[slot].execute()
r.undoCommand = r.OffCommands[slot]
}
func (r *RemoteController) undoButtonWasPushed() {
r.undoCommand.undo()
}
func NewRemoteController() *RemoteController {
size := 7
onCmd := make([]CommandHead, size)
offCmd := make([]CommandHead, size)
for i := range onCmd {
onCmd[i] = &NoCommand{}
offCmd[i] = &NoCommand{}
}
return &RemoteController{onCmd, offCmd, &NoCommand{}}
}
type LightOffCommand struct {
light Light
}
func (l *LightOffCommand) execute() {
l.light.off()
}
func (l *LightOffCommand) undo() {
l.light.on()
}
type Stereo struct {
cd, dvd string
volume int
}
func (s *Stereo) On() {
fmt.Println("오디오 를 켭니다.")
}
func (s *Stereo) Off() {
fmt.Println("오디오 를 끕니다.")
}
func (s *Stereo) SetCd(cd string) {
s.cd = cd
fmt.Printf("cd 를 녛습니다 : %s\n", s.cd)
}
func (s *Stereo) SetDvd(dvd string) {
s.dvd = dvd
fmt.Printf("dvd 를 넣습니다 : %s\n", s.dvd)
}
func (s *Stereo) SetVolume(v int) {
s.volume = v
fmt.Printf("볼륨을 설정합니다 : %d\n", s.volume)
}
type StereoOnWithCDCommand struct {
stereo Stereo
}
func (s *StereoOnWithCDCommand) execute() {
s.stereo.On()
s.stereo.SetCd(s.stereo.cd)
s.stereo.SetVolume(11)
}
func (s *StereoOnWithCDCommand) undo() {
s.stereo.Off()
}
func NewStereoOnWithCDCommand(cd string) *StereoOnWithCDCommand {
return &StereoOnWithCDCommand{
Stereo{
cd: cd,
},
}
}
type StereoOffWithCDCommand struct {
stereo Stereo
}
func (s *StereoOffWithCDCommand) execute() {
s.stereo.Off()
}
func (s *StereoOffWithCDCommand) undo() {
s.stereo.On()
s.stereo.SetCd(s.stereo.cd)
s.stereo.SetVolume(11)
}
const (
Off = iota
Low
Medium
High
)
type CeilingFan struct {
Speed int
Location string
}
func (c *CeilingFan) high() {
c.Speed = High
}
func (c *CeilingFan) medium() {
c.Speed = Medium
}
func (c *CeilingFan) low() {
c.Speed = Low
}
func (c *CeilingFan) off() {
c.Speed = Off
}
func (c *CeilingFan) getSpeed() int {
return c.Speed
}
func NewCeilingFan(location string) *CeilingFan {
return &CeilingFan{Off, location}
}
type CeilingFanHighCommand struct {
fan *CeilingFan
prevSpeed int
}
func (c *CeilingFanHighCommand) execute() {
c.prevSpeed = c.fan.getSpeed()
c.fan.high()
}
func (c *CeilingFanHighCommand) undo() {
switch c.prevSpeed {
case High:
c.fan.high()
case Medium:
c.fan.medium()
case Low:
c.fan.low()
default:
c.fan.off()
}
}
type CeilingFanMediumCommand struct {
fan *CeilingFan
prevSpeed int
}
func (c *CeilingFanMediumCommand) execute() {
c.prevSpeed = c.fan.getSpeed()
c.fan.medium()
}
func (c *CeilingFanMediumCommand) undo() {
switch c.prevSpeed {
case High:
c.fan.high()
case Medium:
c.fan.medium()
case Low:
c.fan.low()
default:
c.fan.off()
}
}
type CeilingFanOffCommand struct {
fan *CeilingFan
prevSpeed int
}
func (c *CeilingFanOffCommand) execute() {
c.prevSpeed = c.fan.getSpeed()
c.fan.off()
}
func (c *CeilingFanOffCommand) undo() {
switch c.prevSpeed {
case High:
c.fan.high()
case Medium:
c.fan.medium()
case Low:
c.fan.low()
default:
c.fan.off()
}
}
type MacroCommand struct {
cmd []CommandHead
}
func (m *MacroCommand) execute() {
for _, v := range m.cmd {
v.execute()
}
}
func (m *MacroCommand) undo() {}
실제 동작하는 "light", "grageDoor", "stereo", "ceilingFan"을 두고 interface 커맨드 헤더를 기점으로 각각의 구조체들을 on, off를 모두 생성한다.
이에 따라 아래와 같은 테스트 코드 작성이 가능해지며, 원하는 기능을 커맨드로 추상화해서 작동하는 것 이 가능해진다.
func Test01(t *testing.T) {
remote := &SimpleController{&LightOnCommand{Light{}}}
remote.ButtonWasPressed()
garage := &GarageOnCommand{&GarageDoor{}}
remote.SetCommand(garage)
remote.ButtonWasPressed()
}
func Test02(t *testing.T) {
rmt := NewRemoteController()
light := &Light{}
garage := &GarageDoor{}
stereo := &Stereo{}
rmt.SetCommand(0, &LightOnCommand{*light}, &LightOffCommand{*light})
rmt.SetCommand(1, &GarageOnCommand{garage}, &GarageDoorOffCommand{garage})
rmt.SetCommand(2, NewStereoOnWithCDCommand("guiwoo"), &StereoOffWithCDCommand{*stereo})
rmt.OnButtonWasPushed(1)
rmt.OffButtonWasPushed(1)
}
func Test03(t *testing.T) {
rmt := NewRemoteController()
light := &Light{}
garage := &GarageDoor{}
stereo := &Stereo{}
rmt.SetCommand(0, &LightOnCommand{*light}, &LightOffCommand{*light})
rmt.SetCommand(1, &GarageOnCommand{garage}, &GarageDoorOffCommand{garage})
rmt.SetCommand(2, NewStereoOnWithCDCommand("guiwoo"), &StereoOffWithCDCommand{*stereo})
rmt.OnButtonWasPushed(1)
rmt.undoButtonWasPushed()
fmt.Println(rmt.undoCommand)
}
func Test04(t *testing.T) {
rmt := NewRemoteController()
fan := NewCeilingFan("Living Room")
rmt.SetCommand(0, &CeilingFanMediumCommand{fan: fan}, &CeilingFanOffCommand{fan: fan})
rmt.SetCommand(1, &CeilingFanHighCommand{fan: fan}, &CeilingFanOffCommand{fan: fan})
}
func Test05_macro(t *testing.T) {
light := &Light{}
grage := &GarageDoor{}
stereo := &Stereo{}
//on command 작성
lightOn := &LightOnCommand{*light}
grageOn := &GarageOnCommand{grage}
stereoOn := NewStereoOnWithCDCommand("guiwoo")
onCmd := []CommandHead{lightOn, grageOn, stereoOn}
//off command 작성
lightOff := &LightOffCommand{*light}
grageOff := &GarageDoorOffCommand{grage}
stereoOff := &StereoOffWithCDCommand{*stereo}
offCmd := []CommandHead{lightOff, grageOff, stereoOff}
//slot 에 넣어주기
rmt := NewRemoteController()
rmt.SetCommand(0, &MacroCommand{onCmd}, &MacroCommand{offCmd})
rmt.OnButtonWasPushed(0)
}
커맨드 패턴을 구성하는 데 있어 지금까지 디자인 패턴 중 추상팩토리 다음으로 구조체를 많이 사용했다.
구조체가 생각보다 많이 작성되어 실제 서비스에서는 관리하기 생각보다 까다롭지 않을까?
음식점에서 주문을 하는 상황에 있다고 가정해 보자. 각 주문은 커맨드의 구상객체 가 될 수 있고,
각 주문은 요리 메서드를 가지고 있다고 하면 이 주문이 요리사에게 전달되어 요리사는 이를 실행할 수 있다.
여기서 자세하게 분리해서 보면
"주문 은 요리로 부터 분리 있고, 요리사는 어떤 주문 이든 바당서 요리를 할수 있다 왜? 요리라는 메서드가 각 주문 별로 있기 때문에"
이러한 것을 요리메서드는 주문커맨드 객체에 의해 매개변수화 되었다고 한다.
이러한 관점에서 실제 코드 설계 과정에서 매개변수화가 필요한 경우가 종종 있다.
위에서 말했지만 애플리케이션의 api 호출에 따라 다른 행위가 실행되게 설계되도록 할 수 있다. 이 경우 각 행위를 커맨드 객체로 만들고, api 호출에 따라 이를 실행할 수 있다.
type Command interface {
Execute(w http.ResponseWriter, r *http.Request)
}
type MyHandler struct {
routes map[string]Command
}
func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if cmd, ok := m.routes[path]; ok {
cmd.Execute(w, r)
} else {
http.NotFound(w, r)
}
}
type FooHandler struct{}
func (f *FooHandler) Execute(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "This is Foo Handler")
}
func callPureHttp(port string) {
handler := &MyHandler{
routes: map[string]Command{
"/foo": &FooHandler{},
},
}
log.Fatal(http.ListenAndServe(":4000", handler))
}
사실 이렇게 놓고 보자니깐 코드 가 동적으로 변경되는것이 전략패턴과 상당히 유사해 보이지 않는가 ?
전략패턴 과 커맨드패턴의 구조차제는 매우 유사하다. 다만 해결하려는 문제 와 사용하는 문맥에 따라 이 패턴의 이름이 갈린다.
처리하고자 하는 도작을 캡슐화 하고 있기에 보다 커맨드 패턴에 가깝다고 할수 있다.
전략패턴은 "행동을 동적으로 변경하고, 알고리즘의 변형을 유연하게 관리해야 하는 경우 사용 된다."
위와 동일한 역할을 하는 전략패턴을 적용해서 구현해보자.
type Strategy interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type StrategyA struct{}
func (s *StrategyA) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}
type StrategyB struct{}
func (s *StrategyB) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi, you've requested: %s\n", r.URL.Path)
}
type Context struct {
strategy Strategy
}
func (c *Context) ExecuteStrategy(w http.ResponseWriter, r *http.Request) {
c.strategy.ServeHTTP(w, r)
}
func callStrategy(){
strategyA := &StrategyA{}
strategyB := &StrategyB{}
context := &Context{}
http.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) {
context.strategy = strategyA
context.ExecuteStrategy(w, r)
})
http.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) {
context.strategy = strategyB
context.ExecuteStrategy(w, r)
})
http.ListenAndServe(":8080", nil)
}
명확하게 다르다 하나의 문맥을 기준으로 매번 새로운 함수 핸들러를 넣어주지만
커맨드 패턴은 이미 정의되어있는 것을 들고와서 사용하는 참 아다르고 어다른 디자인 패턴이다.
어이가 없다.
전략패턴 은 "어떻게 실행하는 가 ? 에 초점을 두고 있고", 커맨드패턴 은 "실행할 행동 ?" 에 초점을 두고 있다.
'Go > Design Pattern' 카테고리의 다른 글
[Design Pattern] 구조패턴-퍼사드 패턴 (Facade Pattern) (0) | 2023.07.28 |
---|---|
[Design Pattern] 구조패턴-어댑터 패턴 (Adapter Pattern) (0) | 2023.07.23 |
[Design Pattern] 구조패턴-데코레이터 패턴 (Decorator Pattern) (0) | 2023.07.10 |
[Design Pattern] 행동패턴-옵저버패턴 (Observer Pattern) (0) | 2023.07.03 |
[Design Pattern] 행동패턴-전략패턴 (Strategy Pattern) (0) | 2023.07.02 |