2024년 1월 10일 작성
Observer Pattern - 객체 상태 변화 알아채기
Observer Pattern은 관찰자들이 관찰하고 있는 대상자의 상태 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 pattern입니다.
Observer Pattern - 객체 상태 변화에 대한 알림 받기
-
Observer Pattern은 객체의 상태 변화를 관찰하는 객체들에게 자동으로 알리는 방식으로 객체 간의 결합도를 낮추고 상호작용을 관리하는 design pattern입니다.
- Observer Pattern에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 모든 객체들한테 연락이 가고, 자동으로 내용이 갱신되는 방식으로 작동합니다.
- 따라서, 객체들 사이에서 일대다(one-to-many) 의존성을 정의하게 됩니다.
- Observer Pattern에서 변하는 것은 ‘subject의 상태’와 ‘observer의 개수, 형식’ 뿐입니다.
- 변하는 부분을 변하지 않는 부분으로부터 분리시켜서, subject를 바꾸지 않고도 subject의 상태에 의존하는 객체들을 바꿀 수 있습니다.
- subject와 observer 사이의 관계는 상속이 아니라 구성에 의해서 이루어집니다.
- Subject는 data의 주인입니다.
- Observer Pattern에서 상태를 저장하고 지배합니다.
- 합성(composition)을 활용하여 observer들을 관리합니다.
- Observer는 관찰자입니다.
- data의 주인은 subject이기 때문에 subject에게 의존성을 가집니다.
- observer가 또 다른 객체의 subject가 될 수도 있습니다.
- observer는 계속해서 정보를 받을지 여부를 실행 중에 결정할 수 있습니다.
- observer와 subject의 관계 예시로 ‘신문 구독’을 들 수 있습니다.
- 구독자(observer)가 신문을 구독(observer 등록)을 하면, 출판사(subject)는 구독자(observer)에게 신문(data)을 보내줍니다.
- 구독자(observer)가 신문 구독을 해지(observer 해제)하면, 출판사(subject)는 더 이상 구독자(observer)에게 신문(data)을 보내지 않습니다.
Class 구조
classDiagram
class Subject {
<<interface>>
registerObserver()
removeObserver()
notifyObservers()
}
class Observer {
<<interface>>
update()
}
class ConcreteSubject {
registerObserver()
removeObserver()
notifyObserver()
getState()
setState()
}
class ConcreteObserver {
update()
etc()
}
Subject <|.. ConcreteSubject
Observer <|.. ConcreteObserver
Subject --> Observer : observer
ConcreteObserver --> ConcreteSubject : subject
note for ConcreteObserver "구현한 observer에서 사용할 기타 method들을 추가할 수도 있습니다."
Observer Parttern의 느슨한 결합
- observer pattern에서는 subject와 obserer가 느슨하게 결합되어 있는 객체 design을 제공합니다.
- “두 객체가 느슨하게 결합(loose coupling)되어 있다.”는 말은 “그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다”는 말과 같습니다.
- subject가 observer에 대해서 아는 것은 observer가 특정 interface(observer interface)를 구현한다는 것 뿐입니다.
- observer의 concrete class가 무엇인지, observer가 무엇을 하는지 등에 대해서는 알 필요가 없습니다.
- observer는 언제든지 새로 추가할 수 있습니다.
- subject는 observer interface를 구현하는 객체의 목록에만 의존하기 때문에 언제든지 새로운 observer를 추가할 수 있습니다.
- 실행 중에 observer를 다른 observer로 바꿔도 됩니다.
- 이렇게 해도 subject 객체는 계속해서 data를 보낼 수 있습니다.
- 또한, obsever를 아무때나 제거해도 됩니다.
- 새로운 형식의 observer를 추가하려고 할 때도 subject를 전혀 변경할 필요가 없습니다.
- 새로운 class에서 observer interface를 구현하고 observer로 등록하기만 하면 됩니다.
- observer interface만 구현한다면 어떤 객체든지 연락을 하기 때문입니다.
- subject와 observer는 서로 독립적으로 재사용할 수 있습니다.
- subject와 observer를 다른 용도로 활용할 일이 있다고 해도, 쉽게 재사용할 수 있습니다.
- 서로 단단하게 결합되어 있지 않기 때문입니다.
- subject나 observer가 바뀌더라도 서로한테 영향을 미치지 않습니다.
- 서로 느슨하게 결합되어 있기 때문에 subject 또는 observer interface를 구현한다는 조건만 만족된다면 변경에 의한 문제는 발생하지 않습니다.
Example : Weather Monitoring Application
- System은 세 가지 요소로 구성됩니다.
- 기상 Station : 실제 기상 정보를 수집하는 장비.
- sensor(온도, 습도, 압력)로부터 측정값을 수집합니다.
- WeatherData 객체 : 기상 station으로부터 오는 data를 추적하는 객체.
- 기상 station 장비 자체로부터 data를 가져올 수 있습니다.
- 모든 상태를 가지고 있습니다.
- Display : 사용자에게 현재 기상 조건을 보여주는 display.
- 현재 조건(온도, 습도, 압력) 화면.
- 기상 통계 화면.
- 기상 예보 화면.
- …
- WeatherData 객체가 subject이고, Display가 observer입니다.
- WeatherData class의 상태가 바뀌면 Display에 알려줍니다.
- WeatherData와 Display는 일대다 관계입니다.
WeatherData : Display = subject : observer = 1 : N
관계입니다.
Class Diagram
classDiagram
class Subject {
<<interface>>
registerObserver()
removeObserver()
notifyObservers()
}
class Observer {
<<interface>>
update()
}
class DisplayElement {
<<interface>>
display()
}
class WeatherData {
registerObserver()
removeObserver()
notifyObserver()
getTemperature()
getHumidity()
getPressure()
measurmentsChanged()
}
class CurrentConditionsDisplay {
update()
display()
}
class StatisticsDisplay {
update()
display()
}
class ForecastDisplay {
update()
display()
}
class ThirdPartyDisplay {
update()
display()
}
Subject <|.. WeatherData
Observer <|.. CurrentConditionsDisplay
Observer <|.. StatisticsDisplay
Observer <|.. ForecastDisplay
Observer <|.. ThirdPartyDisplay
DisplayElement <|.. CurrentConditionsDisplay
DisplayElement <|.. StatisticsDisplay
DisplayElement <|.. ForecastDisplay
DisplayElement <|.. ThirdPartyDisplay
Subject --> Observer : observer
WeatherData <-- CurrentConditionsDisplay : subject
WeatherData <-- StatisticsDisplay : subject
WeatherData <-- ForecastDisplay : subject
WeatherData <-- ThirdPartyDisplay : subject
note for CurrentConditionsDisplay "현재 측정값을 화면에 표시합니다."
note for StatisticsDisplay "평균/최저/최고 수치를 표시합니다."
note for ForecastDisplay "기상 예보를 표시합니다."
note for ThirdPartyDisplay "측정값을 바탕으로 다른 내용을 표시합니다."
Main
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay =
new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
weatherData.removeObserver(forecastDisplay);
weatherData.setMeasurements(62, 90, 28.1f);
}
}
Interface
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
public interface DisplayElement {
public void display();
}
Subject Interface 구현
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<Observer>();
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void measurementsChanged() {
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
Display 구현
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Current conditions: " + temperature
+ "F degrees and " + humidity + "% humidity");
}
}
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = 0.0f;
private float minTemp = 200;
private float tempSum = 0.0f;
private int numReadings;
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure) {
tempSum += temp;
numReadings++;
if (temp > maxTemp) {
maxTemp = temp;
}
if (temp < minTemp) {
minTemp = temp;
}
display();
}
public void display() {
System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
+ "/" + maxTemp + "/" + minTemp);
}
}
public class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.92f;
private float lastPressure;
private WeatherData weatherData;
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
public void display() {
System.out.print("Forecast: ");
if (currentPressure > lastPressure) {
System.out.println("Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("More of the same");
} else if (currentPressure < lastPressure) {
System.out.println("Watch out for cooler, rainy weather");
}
}
}
Reference
- Head First Design Patterns (도서) - Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra