CHAPTER 02.옵저버 패턴
읽은 책 정리/Head Firstr Design Pattern

CHAPTER 02.옵저버 패턴

옵저버 패턴 이해를 위한 간단한 문제 제안

  • 독자에게 기상 조건인 온도, 습도, 기압 3가지의 변화를 감지하는 시스템을 만들라는 요구사항이 들어옵니다.
  • 위의 3가지 항목들은 실시간으로 갱신되야 합니다.
    • 디스플레이를 손쉽게 추가할 수 있어야 합니다.이 기기는 다른 추가사항 또한 바로 넣을 수 있도록 확장 가능해야 합니다.

  • 요구 사항을 좀 더 자세히 설명해보겠습니다.
  • 저희들이 구현해야하는 것은 WeatherData 객체입니다.
  • 가상 스테이션은 습도, 온도, 기압 센서로부터 데이터를 받습니다. 이때 변경되는 데이터는 실시간으로 받기 때문에 WeatherData객체는 실시간으로 데이터를 변경해줘야합니다.
  • WeatherData객체는 가상 스테이션과 통신해서 기상 데이터를 가져옵니다.
  • WeatherData객체는 디스플레이 장비에 현재 조건, 기상통계, 기상 예보중 원하는 내용을 선택해서 표시할 수 있습니다.

클래스로 살펴보기

  • 위의 WeatherData 객체의 get 메소드는 각각의 기상정보를 가져오는 메소드입니다.
  • weatherData에서 갱신된 값을 가져올때마다 mesarementsChanged() 메소드가 실행됩니다.
  • 현재 조건, 기상 통계, 기상 예보를 보여주는 3가지 디스플레이가 업데이트 되도록 아mesarementsChanged()를 변경하도록 해보겠습니다.

문제점 발생

public class WeatherData extends Observable {
	
	public void measurementsChanged() {
		
		//메소드를 호출해서 측정값을 가져온다.
		float temp = getTemperature();
		float humidity = getHumidity();
		float pressure = getPressure();

		//각 디스플레이를 갱신
		currentConditionDisplay.update(tempm, humidity, pressure);
		statisticDisplay.update(temp, humidity, pressure);
		forecastDisplay.update(temp, humidity, pressure);
	}
	
}
  • 이렇게 있는 그대로를 구현했는데 아래와 같은 문제가 존재합니다.
    • 구체적인 구현에 맞췄기 때문에 다른 디스플레이를 추가하거나 제가할 때 코드를 변경해야 합니다.
    • 디스플레이를 갱신하는 3가지 메소드 모두 각은 인터페이스를 사용중이지만 실행중에 디스플레이를 빼거나 더할 수있게 하기위해서는 어떻게 해야할 지 모르겠습니다.
    • 그러므로 캡슐화의 필요성이 있어보입니다.
  • 옵저버 패턴을 도입하면서 위의 문제점들을 해결해보겠습니다.

옵저버 패턴 도입

  • 옵저버 패턴의 성격은 일대단 의존성을 가지며 정의는 한 객체가 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식입니다.
  • 옵저번 패턴은 신문사에 비유하면 이해하기 쉽습니다.
  • 신문사와 구독자는 신문사를 구독하는 관계를 유지한다면 신문사는 신문을 만들때만 구독자에게 알려주어 구독자가 신문을 읽을수 있게 하면 됩니다.
  • 이때 구독하는 대상이 되는 것을 주제(ex.신문사)라 부릅니다.
  • 구독을 하는 주체를 옵저버(ex.구독자)라 부릅니다.

느슨한 결합이란

  • 느슨한 결합은 모듈별로 서로가 잘모르는 관계를 의미합니다.
  • 옵저버 패턴에서의 느슨한 결합의 예시는
    • 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 압니다.
    • 옵저버는 언제든지 새로 추가할 수 있습니다.
      • 주제는 옵저버 인터페이스에만 의존하므로 언제든지 새로운 옵저버를 추가할 수 있다.
      • 또한 다른 옵저버로도 쉽게 변경이가능하며 제거도 가능하다.
    • 새로운 형식의 옵저버를 추가할 때도 주제블 변경할 필요가 없다.
    • 주제와 옵저버는 서로 독립적으로 재사용이 가능하다.
    • 주제나 옵저버가 달려져도 서로에게 영향을 미치지 않는다.
  • 상호작용하는 개체 사이에는 가능하면 느슨한 결합을 사용해야 합니다.

옵저버 패턴을 활용한 가상 스테이션 설계하기

  • 앞서 배운 전략패턴을 응용해서 각각의 기능을 나타내는 최소한의 인터페이스로 정의를 했습니다.
public interface Subject {
	public void registerObserver(Observer o);
	public void removeObserver(Observer o);
	public void notifyObservers();
}
  • Subject는 Observer를 인자로 받고 각각의 Observer가 실행하는 3개의 메소드를 정의했습니다.
public interface Observer {
	public void update(float temp, float humidity, float pressure);
}
  • Observer에는 기상 정보가 변경되었을 때 옵저버에게 전달되는 상태값이다.
public interface DisplayElement {
	public void display();
}
  • DisplayElement는 항목을 표시하는 display() 메소드 밖에 없다.
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);
	}

	//옵저버가 탈퇴를 요청하면 List에서 빼기만 하면 된다.
	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;
	}

}
  • WeatherData를 구현합니다.
  • WeatherData는 Subject를 구현합니다.
  • 자세한 설명은 코드에 주석을 첨부하였습니다.

디스플레이 요소 구현하기

//화면 설정 정보를 갱신하기 위해 Observer를 정의, 정보를 보여주기 위해 DisplayElement를 정의
public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	private WeatherData weatherData;
	
	//생성자에 WeatherData 객체를 생성할때 전달하며 Observer에 등록한다.
	public CurrentConditionsDisplay(WeatherData weatherData) {
		this.weatherData = weatherData;
		weatherData.registerObserver(this);
	}
	
	//update를 호출하며 온도와 습도를 저장하고 display()를 호출한다.
	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");
	}
}
  • CurrentConditionsDisplay 객체를 구현합니다.
  • CurrentConditionsDisplay는 현재의 화면에 대한 설정 정보를 갱신해야 하므로 Observer를 정의합니다.
  • 또한 보여주는 기능을 실행해야하기 때문에 DisplayElement를 정의합니다.
  • 이외의 기능은 코드에 주석을 첨부하였습니다.
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");
		}
	}
}
  • 응용해서 평균 최저를 보여주는 StatisticsDisplay를 구현합니다.
  • 응용해서 기상예보를 보여주는 ForecastDisplay를 구현합니다.
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);
	}
}
  • 테스트를 위한 WeatherStation입니다.

실행결과

정리

  • 바뀌는 부분은 캡슐화해야 합니다.
  • 상속보다는 구성을 활용해야합니다.
  • 구현보다는 인터페이스에 맞춰서 프로그래밍해야 합니다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 합니다.
    • 이는 주제와 옵저버간의 관계의 결합도를 낮춰야한다는 의미입니다.
  •