본문 바로가기
golang/design pattern

golang design pattern #13 Observer

by PudgeKim 2021. 11. 23.

Observer 패턴은 특정 이벤트나 상태가 변경되었을 경우 알림을 받고 싶을 때 사용되는 패턴입니다.

이 패턴을 구현하기 위해서는 보통 Observable(알림을 주는 객체)와 Observer(알림을 받는 객체)를 구현하게 됩니다.

흔히 아는 pub/sub 패턴으로도 많이 사용되는 패턴입니다.

 

예를 들어 환자와 간호사가 있는데 1명의 간호사는 여러 명의 환자들을 관리하고 간호사들은 환자의 열과 심박수를 체크합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
type Observable interface {
    subscribe(observer Observer)
    unsubscribe(observer Observer)
    notifyAll(data interface{})
}
 
type Patient struct {
    name string
    temperature float64
    heartRate int
    observerList []Observer
}
 
func NewPatient(name string, temperature float64, heartRate int) *Patient {
    return &Patient{
        name,
        temperature,
        heartRate,
        make([]Observer, 0),
    }
}
 
func (p *Patient) subscribe(observer Observer) {
    p.observerList = append(p.observerList, observer)
}
 
func (p *Patient) unsubscribe(observer Observer) {
    removedIdx := -1
 
    for i:=0; i<len(p.observerList); i++ {
        if p.observerList[i].getId() == observer.getId() {
            removedIdx = i
            break
        }
    }
 
    if removedIdx == -1 {
        return
    } else {
        p.observerList[removedIdx] = p.observerList[len(p.observerList)-1]
        p.observerList = p.observerList[:len(p.observerList)-1]
    }
}
 
func (p *Patient) notifyAll(data interface{}) {
    for _, observer := range p.observerList {
        observer.update(data)
    }
}
cs

Observable 인터페이스와 해당 인터페이스를 구현한 환자 구조체입니다.

사실 이번 예제에서는 Observable 타입은 사용되지 않지만 이렇게 인터페이스를 선언하면 조금 더 유연한 코드를 작성할 수 있습니다.

unsubscribe 코드가 조금 이해가 안될 수도 있는데 Patient 구조체의 observerList에는 간호사가 들어가게 될텐데
해당 리스트에서 간호사의 순서는 상관이 없으므로 remove하려는 간호사를 뺀 후 다시 slice를 만드는 것보다는 위 코드처럼하면 훨씬 더 빠른 코드를 작성할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
func (p *Patient) setTemperature(temperature float64) {
    p.temperature = temperature
    medicalInfo := NewMedicalInfo(p.name, TEMPERATURE, p.temperature)
    p.notifyAll(medicalInfo)
}
 
func (p *Patient) setHeartRate(heartRate int) {
    p.heartRate = heartRate
    medicalInfo := NewMedicalInfo(p.name, HEARTRATE, p.heartRate)
    p.notifyAll(medicalInfo)
}
cs

여기까지 Patient 구조체의 코드입니다. 환자의 온도와 심박수가 변한다면 간호사는 변화를 보고 어떠한 조치를 취해야하기 때문에 온도와 심박수를 set한 후에 notifyAll을 통해 알려줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const (
    TEMPERATURE = iota
    HEARTRATE
)
 
type MedicalInfo struct {
    PatientName string
    Kind        int
    Value       interface{}
}
 
func NewMedicalInfo(name string, kind int, value interface{}) MedicalInfo {
    return MedicalInfo{
        name,
        kind,
        value,
    }
}
cs

간호사 관련 코드를 보기전에 간호사가 환자로부터 받게 될 정보에 관련된 코드입니다.

iota를 이용하여 환자가 온도 관련 정보를 보냈는지 심박수 관련 정보를 보냈는지를 알 수 있고 
온도는 float64 타입이고 심박수는 int 타입이기 때문에 Value는 interface{}로 정의하였습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Observer interface {
    update(data interface{})
    getId() string
}
 
type Nurse struct {
    name string
    id string
}
 
func NewNurse(name string) *Nurse {
    id := xid.New().String()
    return &Nurse{
        name: name,
        id: id,
 
    }
}
 
func (n *Nurse) update(data interface{}) {
    if medicalInfo, ok := data.(MedicalInfo); ok {
        switch medicalInfo.Kind {
        case TEMPERATURE:
            if medicalInfo.Value.(float64) > 38 {
                fmt.Printf("%s goes to %s\n", medicalInfo.PatientName, n.name)
            }
        case HEARTRATE:
            if medicalInfo.Value.(int) < 30 {
                fmt.Printf("%s calls a doctor\n", n.name)
            }
        }
    }
}
 
func (n *Nurse) getId() string {
    return n.id
}
cs

이제 간호사 관련 구조체 코드입니다.

각 간호사를 식별하기 위해 랜덤한 id를 부여합니다.

그리고 update에서는 interface 형변환을 통해 환자가 어떤 정보를 보냈는지 파악하고, 고열이 나거나 심박수가 너무 낮으면 간호사는 어떠한 조치를 취하게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
    p1 := NewPatient("bob"36.560)
    p2 := NewPatient("tony"36,  80)
    n1 := NewNurse("sarah")
    n2 := NewNurse("christina")
 
    p1.subscribe(n1)
    p1.subscribe(n2)
    fmt.Println(p1.observerList)
 
    p2.subscribe(n2)
 
    p1.setTemperature(40)
    p2.setHeartRate(20)
}
 
cs

위 같은 코드를 작성하면 bob의 온도는 40도가 되어 담당 간호사들에게 알림이 가고 간호사는 조치를 취하게 됩니다.

tony 역시 심박수가 낮아져서 간호사에게 알림이 갑니다.