에릭 프리먼의 <헤드 퍼스트 디자인 패턴>을 읽고 정리한 후,
Swift 로 적용해보는 스터디입니다.
기상 모니터링 애플리케이션은 기상 스테이션, WeatherData 객체, 디스플레이로 이루어져있습니다.
💡 이제 WeatherData 객체를 사용하여
현재 조건(온도, 습도, 압력)
,기상 통계
,기상 예측
이렇게 세 항목을 보여주는 기상 모니터링 애플리케이션을 만들어야 합니다.
// 기상 관측값이 갱신될 때 마다 알려주기 위한 메소드
public void measurementsChanged() { }
measurementsChanged()
를 현재 조건
, 기상 통계
, 기상 예측
이렇게 세가지 디스플레이를 갱신할 수 있도록 구현해야합니다.
그리고 시스템이 확장 가능해서 다른 개발자들이 별도의 디스플레이 항목을 만들 수 있도록 해야합니다.
public class WeatherData {
public void measurementChanged() {
float temp = getTemperture();
float humidity = getHumidity();
float pressure = getPressure();
// 각 디스플레이 항목을 불러서 디스플레이를 갱신하도록 하는 메소드
currentConditionDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
}
구체적인 구현에 맞춰서 코딩했기 때문에 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가, 제거할 수 없습니다.
→ 바뀔 수 있는 부분을 캡슐화 해야합니다.
옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
신문 구독 매커니즘과 동일합니다.
출판사를 주제 Subject
, 구독자를 옵저버 Observer
라고 부릅니다.
옵저버 객체들은 주제
객체를 구독하고(등록되어 있으며)
주제
의 데이터가 바뀌면 갱신 내용을 전달받습니다.
옵저버를 등록하고 해제 할 수 있으며 옵저버가 주제가 될 수도 있습니다.
옵저버 패턴을 구현하는 방법에는 여러 가지가 있지만,
대부분 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인을 바탕으로 합니다.
두 객체가 느슨하게 결합되어 있다는 것은, 그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미합니다. 옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.
디자인 원칙
서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야한다.
느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있습니다.
객체 사이의 상호의존성을 최소화할 수 있기 때문입니다.
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
}
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
public interface DisplayElement {
public void display();
}
public class WeatherData impelements Subject {
private ArrayList observers; // Observer 객체들 저장
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList();
}
public void registerObserver(observer o) {
observer.add(o);
}
public void removeObserver(observer o) {
int i = observers.indexof(o);
if (i >= 0) {
observers.remove(i);
}
}
public void notifyObservers() {
for (int i = 0; i < observer.size(); i ++) {
Observer observer = (Observer)observer.get(i);
observer.update(temperature, humidity, pressure);
}
}
public void measurementsChanged(float temperature,
float humidity,
float pressure) {
notifyObservers();
}
public void setMeasurements(float temperature,
float humidity,
float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
public class CurrentConditionDisplay implements Observer, DisplayElement {
private float temperture;
private float humidity;
private float pressure;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = wheaterData;
weahterData.registerObserver(this);
}
public void update(float temperture, float humidity, float pressure) {
this.temperture = temperture;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.printIn("현재 조건은" + temperture + humidity + humidity);
}
}
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
Observer currentConditionsDisplay = new CurrentConditionsDisplay();
Observer statisticsDisplay = new StatisticsDisplay();
weatherData.registerObserver(currentConditionsDisplay);
weatherData.setWeatherData(3, 5, 7);
System.out.println("통계 디스플레이를 추가합니다.");
weatherData.registerObserver(statisticsDisplay);
System.out.println("기상 데이터가 업데이트 됩니다.");
weatherData.setWeatherData(20, 30, 80);
System.out.println("현재 상태 디스플레이를 제거합니다.");
weatherData.removeObserver(currentConditionsDisplay);
weatherData.setWeatherData(25, 30, 80);
}
}
지금 만들어 놓은 WeatherData 디자인은 하나의 데이터만 갱신해도 되는 상황에서도 update 메소드에 모든 데이터를 보내도록 되어 있습니다.
하지만 풍속 같은 새로운 데이터가 추가되면, 대부분의 update
메소드에서 풍속 데이터를 쓰지 않더라도 모든 디스플레이에 있는 update
메소드를 바꿔야 할 수도 있습니다.
옵저버 패턴에는 두가지 방식이 있습니다.
PUSH
방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식PULL
방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식사실 주제가 옵저버로 데이터를 보내는 PUSH
방식을 사용하거나, 옵저버가 주제로부터 데이터를 당겨오는 PULL
방식을 사용하는 방법 중 어느 하나를 선택하는 일은 구현 방법의 문제라고 볼 수 있습니다.
// WeatherData.java
public void notifyObservers() {
for(Observer observer: observers) {
observer.update();
}
}
// Observer.java
public interface Observer {
void update();
}
// CurrentConditionDisplay.java
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
}
public void update() {
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
public void display() {
System.out.println("현재 상태: 온도 "+temperature+"F, 습도 "+humidity+"%");
}
}
기상정보가 바뀌면 옵저버들에게 notify되면서 display 되고,
정상적으로 해제되는 것이 보이네요!
📢 다른 객체의 상태가 변경될 때마다 어떤 행동을 하고 싶다면 옵저버 패턴을 사용하면 된다.
이러한 패턴은 iOS에서는 ViewController에 Observer(Subscriber)가 있고, Model에 Subject(Publisher)가 있는 MVC 패턴에서 사용할 수 있습니다.
이를 통해 Model은 ViewController의 타입에 대해 알 필요 없이 상태가 변경될 때마다 이를 ViewController에 전달할 수 있습니다.
따라서 여러 개의 ViewController가 하나의 Model의 변경사항을 사용할 수 있게 됩니다.
발행 구독 패턴은 비동기 메세징 패러다임
입니다.
Subject(Publisher)
가 Observer(Subscriber)
에게 변화를 알려주는 관계Publisher
와 Subscriber
사이에는 브로커(Broker) 혹은 이벤트 버스(Event Bus)로 불리는 계층이 존재