고 언어로 작성된 코드는 최하단에 있습니다.
WeatherData 객체로 현재 조건, 기상 통계, 기상 예보, 이렇게 3가지 항목을 디스플레이 장비에서 갱신해 가면서 보여주는 애플리케이션을 만든다.
디스플레이를 구현하고 새로운 값이 들어올 때 마다, 즉 measurementsChanged() 메소드가 호출될 때마다 WeatherData에서 디스플레이를 업데이트해야 한다.
measurementsChanged()는 어떻게 돌아가는지 모르고 호출된다는 사실만 알고 있음.
기상 데이터를 사용하는 디스플레이 요소 3가지 구현 필요.
measurementsChanged() 메소드에 코드 추가 필요.
추가 디스플레이를 위한 확장성
요구조건을 단순 구현한다면 아래와 같이 나온다.
public class WeatherData {
public void measurementsChanged() {
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionDisplay.update(temp, humidity, pressure);
statisticDisplay.update(temp, humidity, pressure);
forecaseDisplay.update(temp, humidity, pressure);
}
}
하지만 누가 봐도 확장성을 고려하지 않은 단순 코딩이다.
책에서는 주제(subject) + 옵저버(observer) == 옵저버 패턴
이라고 설명하고 있다.
옵저버 패턴은 한 객체의 상태가 바뀌면 극 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 1:N 의존성을 정의한다.
객체들이 상호작용할 수 있지만 서로를 잘 모르는 관계를 의미한다. 유연성이 좋아진다.
Subject는 Observer가 특정 인터페이스를 구현한다는 사실만 안다.
옵저버는 언제든 새로 추가 가능하며 제거 가능하다.
새로운 형식의 옵저버를 추가해도 주제를 변경할 필요가 없다.
주제와 옵저버는 서로 독립적으로 재사용할 수 있다.
주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다.
상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
그럼 어떻게 옵저버 패턴을 적용시켜야할까?
인터페이스부터 구현해보자
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();
}
public class WeatherData implements Subject{
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
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 class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.printf("현재 상태: 온도 %.1fF, 습도 %.1f\n", this.temperature, this.humidity);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
}
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");
}
}
}
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisplay = 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);
//현재 상태: 온도 80.0F, 습도 65.0
//Avg/Max/Min temperature = 80.0/80.0/80.0
//Forecast: Improving weather on the way!
//현재 상태: 온도 82.0F, 습도 70.0
//Avg/Max/Min temperature = 81.0/82.0/80.0
//Forecast: Watch out for cooler, rainy weather
//현재 상태: 온도 78.0F, 습도 90.0
//Avg/Max/Min temperature = 80.0/82.0/78.0
//Forecast: More of the same
}
Java Swing을 구현하는 부분인데, 굳이 사용할 일도 없으며 예전 국비교육에서 지겹도록 만져봤으니 패스하겠다..
Subject의 notifyObservers() 에서 observer.update()메소드가 여러 데이터들을 매개변수로 사용하고 있어 확장성에 문제가 있지 않을까? 라는 생각을 했는데 풀 방식으로 변경하면 해당 부분도 문제를 해결할 수 있으며, 새로운 디스플레이가 추가되고 WeatherData에 새로운 필드를 추가하더라도 각 옵저버에서 필요한 데이터를 뽑아 사용할 수 있기 때문에 확장성도 높아진다.
public interface Observer {
public void update(); //update 내부 매개변수 삭제
}
public class WeatherData implements Subject{
private final List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(); //여기도 매개변수 없도록 수정
}
}
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() { //Getter 추가
return temperature;
}
public float getHumidity() { //Getter 추가
return humidity;
}
public float getPressure() { //Getter 추가
return pressure;
}
}
추가적으로 각 Display 객체의 update() 메소드가 필드의 weatherData 객체에서 Getter 매소드로 값을 주입하도록 수정
JAVA코드를 제가 생각하는 베스트 프렉티스로 작성한 코드로, 정답이 아닙니다.
참고용으로 사용해주시고 잘못된 부분이나 개선이 필요한 점은 댓글로 지적해주시면 감사하겠습니다!
type Subject interface {
RegisterObserver(observer Observer)
RemoveObserver(observer Observer)
SetMeasurements(temperature, humidity, pressure float32)
Temperature() float32
Humidity() float32
Pressure() float32
}
type Observer interface {
Update()
}
type DisplayElement interface {
Display()
}
type weatherData struct {
observer []domain.Observer
temperature float32
humidity float32
pressure float32
}
func InitWeatherData() domain.Subject {
return &weatherData{
observer: make([]domain.Observer, 0),
}
}
func (w *weatherData) RegisterObserver(observer domain.Observer) {
if w.getRegisterObserverIndex(observer) != -1 {
return
}
w.observer = append(w.observer, observer)
}
func (w *weatherData) RemoveObserver(observer domain.Observer) {
idx := w.getRegisterObserverIndex(observer)
if idx == -1 {
return
}
if idx == 0 {
w.observer = w.observer[1:]
return
}
w.observer = append(w.observer[:idx], w.observer[idx+1:]...)
}
func (w *weatherData) SetMeasurements(temperature, humidity, pressure float32) {
w.temperature = temperature
w.humidity = humidity
w.pressure = pressure
w.measurementsChanged()
}
func (w *weatherData) Temperature() float32 {
return w.temperature
}
func (w *weatherData) Humidity() float32 {
return w.humidity
}
func (w *weatherData) Pressure() float32 {
return w.pressure
}
func (w *weatherData) notifyObservers() {
for i := range w.observer {
w.observer[i].Update()
}
}
func (w *weatherData) getRegisterObserverIndex(observer domain.Observer) int {
result := -1
for i := range w.observer {
if w.observer[i] == observer {
result = i
}
}
return result
}
func (w *weatherData) measurementsChanged() {
w.notifyObservers()
}
type currentConditionsDisplay struct {
temperature float32
humidity float32
weatherData domain.Subject
}
func InitCurrentConditionsDisplay(weatherData domain.Subject) domain.Observer {
c := ¤tConditionsDisplay{
weatherData: weatherData,
}
weatherData.RegisterObserver(c)
return c
}
func (d *currentConditionsDisplay) Display() {
fmt.Printf("현재 상태: 온도 %.1fF, 습도 %.1f\n", d.temperature, d.humidity)
}
func (d *currentConditionsDisplay) Update() {
d.temperature = d.weatherData.Temperature()
d.humidity = d.weatherData.Humidity()
d.Display()
}
type forecastDisplay struct {
currentPressure float32
lastPressure float32
weatherData domain.Subject
}
func InitForecastDisplay(weatherData domain.Subject) domain.Observer {
o := &forecastDisplay{
currentPressure: 29.2,
weatherData: weatherData,
}
weatherData.RegisterObserver(o)
return o
}
func (d *forecastDisplay) Display() {
fmt.Print("Forecast: ")
if d.currentPressure > d.lastPressure {
fmt.Println("Improving weather on the way!")
} else if d.currentPressure == d.lastPressure {
fmt.Println("More of the same")
} else if d.currentPressure < d.lastPressure {
fmt.Println("Watch out for cooler, rainy weather")
}
}
func (d *forecastDisplay) Update() {
d.lastPressure = d.currentPressure
d.currentPressure = d.weatherData.Pressure()
d.Display()
}
type statisticsDisplay struct {
maxTemp float32
minTemp float32
tempSum float32
numReadings int
weatherData domain.Subject
}
func InitStatisticsDisplay(weatherData domain.Subject) domain.Observer {
o := &statisticsDisplay{
maxTemp: 0.0,
minTemp: 200,
tempSum: 0.0,
weatherData: weatherData,
}
weatherData.RegisterObserver(o)
return o
}
func (d *statisticsDisplay) Display() {
fmt.Printf("Avg/Max/Min temperature = %.1f / %.1f / %.1f\n",
d.tempSum/float32(d.numReadings), d.maxTemp, d.minTemp)
}
func (d *statisticsDisplay) Update() {
d.tempSum += d.weatherData.Temperature()
d.numReadings++
if d.weatherData.Temperature() > d.maxTemp {
d.maxTemp = d.weatherData.Temperature()
}
if d.weatherData.Temperature() < d.minTemp {
d.minTemp = d.weatherData.Temperature()
}
d.Display()
}
func main() {
weatherData := subject.InitWeatherData()
observer.InitCurrentConditionsDisplay(weatherData)
observer.InitStatisticsDisplay(weatherData)
observer.InitForecastDisplay(weatherData)
weatherData.SetMeasurements(80, 65, 30.4)
weatherData.SetMeasurements(82, 70, 29.2)
weatherData.SetMeasurements(78, 90, 29.2)
//현재 상태: 온도 80.0F, 습도 65.0
//Avg/Max/Min temperature = 80.0 / 80.0 / 80.0
//Forecast: Improving weather on the way!
//현재 상태: 온도 82.0F, 습도 70.0
//Avg/Max/Min temperature = 81.0 / 82.0 / 80.0
//Forecast: Watch out for cooler, rainy weather
//현재 상태: 온도 78.0F, 습도 90.0
//Avg/Max/Min temperature = 80.0 / 82.0 / 78.0
//Forecast: More of the same
}
자바에서와 같은 결과가 나오는걸 확인할 수 있다.