이 포스트는 한빛미디어 'HeadFirst - Design Pattern' 를 공부하면서 작성되었습니다.
! 결합을 느슨하게 하기
두 객체가 느슨하게 연결되어 있으면, 상호작용은 할 수 있으나 서로에 대해 아는 것은 아주 적다.
옵저버 패턴은 subject 와 observer들이 느슨하게 결합된 객체 디자인을 제공한다.
? 왜
subject 가 observer 에 대해 아는 유일한 것은 observer 가 어떤 observer interface 를 구현한다는 사실 뿐이다.
observer 의 concrete class 를 알 필요도 없고, 그게 뭐하는 건지 등등 알 필요가 없다.
언제든지 새로운 observer 를 추가할 수 있다. subject 가 연연하는 것은 observer interface 를 구현하는 object들의 목록 뿐이라서, 우리는 언제든 새로운 observer 를 추가할 수 있다. 사실은 runtime 중에 어떤 observer를 다른 observer로 교체할 수도 있고, 그래도 subject는 계속해서 말을 건낼 수 있다. 아니면, 언제든 observer 를 제거할 수도 있다.
새로운 타입의 observer 를 추가하기 위해 subject를 고칠 필요가 없다. observer 가 되기 위한 새로운 concrete class 가 있다고 가정해보자. 새로운 class 타입을 수용하려고 subject 를 변경할 필요는 없다. 단지 observer interface 를 새로운 class에 구현하고, observer 를 등록하면 된다. subject 는 observer interface 를 구현하는 어떠한 object 에게든 알람을 전달할 것이다.
subject 와 observer 는 서로 독립적으로 재사용할 수 있다. 둘이 서로 단단하게 결합되어 있지 않기 때문!
subject 나 observer 의 변경은 나머지에게 영향을 주지 않는다. 두 object 는 느슨하게 연결되어 있기 때문에, object 가 subject interface 또는 observer interface 를 구현하기만 한다면어느것이든 바꾸기가 자유롭다.
*concrete class (구상클래스)
추상클래스를 상속받아 구체화 한 하위 클래스
*interface (인터페이스)
ex. FlyBehavior, QuackBehavior
완전히 추상클래스임, class 언급 없이 interface 로 정의하고, implements 로 interface 를 구현할 수 있다.
ex. public interface FlyBehavior {} / public class FlyNoWay implements FlyBehavior {}
그러면 날씨 정보를 표출하기 위한 Weather Cast 프로젝트에서 subject 와 observer 를 구분지어 보자.
subject : Weather Station Data
-> subject data - temp, humidity, press / subject methods - regist, remove, update data
observers : Displays (Current/Statistics/Forecast)
-> observer interface : Display / concrete classes : Current/Statisics/Forecast)
근데 이제 observer 는 subject data 를 업데이트 하는 역할을 수행하고, Display interface 가 display 를 수행하는 것으로 나눈다.
그리고 구상클래스들은 update 와 display 를 구현한다.
1. 인터페이스 Subject 와 Observer 와 DisplayElement
package weatherproject;
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
package weatherproject;
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
package weatherproject;
public interface DisplayElement {
public void display();
}
2. Subject 를 구현하는 WeatherData 클래스
package weatherproject;
import java.util.ArrayList;
public class WeatherData implements Subject {
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList();
}
// Subject 인터페이스를 구현 - registerObserver, removeObserver, notifyObservers
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if(i >= 0)
observers.remove(o);
}
// Observer에 측정값이 알려지면 Display 함수 호출
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer)observers.get(i);
// update -> display 호출
observer.update(temperature, humidity, pressure);
}
}
// 기상 스테이션으로 부터 갱신된 측정값들을 가져와서 Observer 들에게 알림
public void measurementsChanged() {
notifyObservers();
}
// 기상 스테이션으로 부터 갱신된 측정값들을 가져옴
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// 기타 WeatherData 메소드
}
3. Observer 와 DisplayElement 를 구현하는 클래스들 - CurrentConditionsDisplay 와 StatisticsDisplay 와 Forecastdisplay
package weatherproject;
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
// Subject 레퍼런스 변수 선언
private Subject weatherData;
// 생성자 - 이 디스플레이 항목을 Observer 로 등록함
public CurrentConditionsDisplay(Subject weatherData) {
// Subject 레퍼런스 변수에 저장 - 나중에 removeObserver 에서 유용할 수 있음 (?)
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("Currenet conditions: " + temperature + "F degrees and " + humidity + "% himidity");
}
}
package weatherproject;
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = 0.0f;
private float minTemp = 200;
private float tempSum= 0.0f;
private int numReadings;
private Subject weatherData;
public StatisticsDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
public void update(float temperature, float humidity, float pressure) {
float temp = temperature;
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);
}
}
package weatherproject;
public class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.92f;
private float lastPressure;
private Subject weatherData;
public ForecastDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
public void update(float temperature, 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");
}
}
}
4. 테스트 !
package weatherproject;
public class WeatherStation {
public static void main(String[] args) {
// Subject의 구상클래스인 WeatherData 의 객체 생성
WeatherData weatherData = new WeatherData();
// Observer, Display 의 구상클래스들의 객체 생성
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
//StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
//ForeCastDisplay forecastDisplay = new ForecastDisplay(weatherData);
// 날씨 정보가 측정된 것처럼 시뮬레이션
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 45, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
결과는 ..

Observer 인터페이스를 구현한 클래스들의 생성자에서 Subject 레퍼런스를 저장하는데, 나중에 Observer 를 삭제하고 싶을 때 유용하다고 한다. 무슨 말인지 잘 모르겠다 ㅎㅎ
* 레퍼런스 타입이란? 자바에서 데이터 타입에는 '기초적인 타입'과 '레퍼런스 타입'이 있다. 기초적인 타입은 우리가 흔히 봐 왔던 int, char, boolean 등이고, 레퍼런스 타입은 클래스 타입/인터페이스 타입/배열 타입/열거 타입 으로 분류할 수 있다.
그런데, Weather-O-Rama 의 CEO 인 자니 허리케인 님이 디스플레이 항목을 추가해 달라고 전화가 왔다고 한다.
체감 온도 디스플레이 항목을 추가할 것이다.
체감 온도란 영어로 heat index 또는 humiture 라고 하고, 기온(T) 과 상대 습도(RH) 를 바탕으로 결정된다고 한다.
heatindex = 어쩌구저쩌구 ..
복잡한 식이다. HeatIndexDisplay.java 파일을 만든 다음 heatindex.txt 파일을 복사해 넣으라고 한다.
이러쿵저러쿵 HeatIndexDisplay.java 을 아래와 같이 구현
package weatherproject;
public class HeatIndexDisplay implements Observer, DisplayElement {
private float heatIndex;
private Subject weatherData;
public HeatIndexDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temperature, float humidity, float pressure) {
float t = temperature;
float rh = humidity;
heatIndex = (float)
(
(16.923 + (0.185212 * t)) +
(5.37941 * rh) -
(0.100254 * t * rh) +
(0.00941695 * (t * t)) +
(0.00728898 * (rh * rh)) +
(0.000345372 * (t * t * rh)) -
(0.000814971 * (t * rh * rh)) +
(0.0000102102 * (t * t * rh * rh)) -
(0.000038646 * (t * t * t)) +
(0.0000291583 * (rh * rh * rh)) +
(0.00000142721 * (t * t * t * rh)) +
(0.000000197483 * (t * rh * rh * rh)) -
(0.0000000218429 * (t * t * t * rh * rh)) +
(0.000000000843296 * (t * t * rh * rh * rh)) -
(0.0000000000481975 * (t * t * t * rh * rh * rh)));
display();
}
public void display() {
System.out.println("Heat index is " + heatIndex);
}
}
테스트 클래스에도 이 새로운 항목의 객체를 생성하고 실행한 결과는,

실행된 결과를 보면, 옵저버1 CurrentConditionDisplay 는 온도와 습도만 필요로 한다. 심지어 옵저버2 StatisticsDisplay 는 온도만, 옵저버3 ForecastDisplay 는 기압만을 필요로 한다. 그러나 옵저버들은 온도,습도,기압 세가지를 모두 업데이트 받는다. Subject 가 그냥 다 보내기 때문이다. 그래서 옵저버는 자신이 필요한 정보만을 가져가기를 원했다.
그러면 자바에 내장되어 있는 옵저버 패턴을 이용하면 된다고 한다는데, 다음 시간에 무슨 얘기인지 알아보기로 한다.
'성장하는 중 입니다? > 디자인패턴' 카테고리의 다른 글
자바 내장 옵저버 패턴 (0) | 2021.07.11 |
---|