[헤드퍼스트 디자인 패턴 chapter02] 옵저버 패턴; 객체들에게 연락 돌리기 (JAVA, GOLANG)

Coen·2024년 7월 6일
1
post-thumbnail

옵저버 패턴

고 언어로 작성된 코드는 최하단에 있습니다.

요구조건

WeatherData 객체로 현재 조건, 기상 통계, 기상 예보, 이렇게 3가지 항목을 디스플레이 장비에서 갱신해 가면서 보여주는 애플리케이션을 만든다.

  1. 디스플레이를 구현하고 새로운 값이 들어올 때 마다, 즉 measurementsChanged() 메소드가 호출될 때마다 WeatherData에서 디스플레이를 업데이트해야 한다.

  2. measurementsChanged()는 어떻게 돌아가는지 모르고 호출된다는 사실만 알고 있음.

  3. 기상 데이터를 사용하는 디스플레이 요소 3가지 구현 필요.

  4. measurementsChanged() 메소드에 코드 추가 필요.

  5. 추가 디스플레이를 위한 확장성

기상 스테이션용 코드 추가하기

요구조건을 단순 구현한다면 아래와 같이 나온다.

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. 새로운 객체를 옵저버에 등록

  1. 새로운 옵저버로 등록이 되고 전달될 값을 기다린다.

  1. 주제의 값이 변경되면 모든 옵저버가 변경된 값을 받는다.

  1. 특정 객체를 옵저버에서 제거

  1. 주제의 값이 변경되면 모든 옵저버가 변경된 값을 받는다. (제거된 객체는 해당 값을 받지 못함)

옵저버 패턴의 정의

옵저버 패턴은 한 객체의 상태가 바뀌면 극 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 1:N 의존성을 정의한다.

옵저버 패턴의 구조

느슨한 결합(Loose Coupling)의 위력

객체들이 상호작용할 수 있지만 서로를 잘 모르는 관계를 의미한다. 유연성이 좋아진다.

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();
}

Subject 인터페이스 구현하기

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을 구현하는 부분인데, 굳이 사용할 일도 없으며 예전 국비교육에서 지겹도록 만져봤으니 패스하겠다..

풀 방식으로 코드 바꾸기 (Refactoring)

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 매소드로 값을 주입하도록 수정

Golang으로 구현한 옵저버 패턴

JAVA코드를 제가 생각하는 베스트 프렉티스로 작성한 코드로, 정답이 아닙니다.
참고용으로 사용해주시고 잘못된 부분이나 개선이 필요한 점은 댓글로 지적해주시면 감사하겠습니다!

인터페이스

subject.go

type Subject interface {
	RegisterObserver(observer Observer)
	RemoveObserver(observer Observer)
	SetMeasurements(temperature, humidity, pressure float32)
	Temperature() float32
	Humidity() float32
	Pressure() float32
}

observer.go

type Observer interface {
	Update()
}

display_element.go

type DisplayElement interface {
	Display()
}

Subject 구현체

weather_data.go

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()
}

Observer 구현체

current_conditions_display.go

type currentConditionsDisplay struct {
	temperature float32
	humidity    float32
	weatherData domain.Subject
}

func InitCurrentConditionsDisplay(weatherData domain.Subject) domain.Observer {
	c := &currentConditionsDisplay{
		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()
}

forecast_display.go

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()
}

statistics_display.go

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()
}

Main method

main.go

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
}

자바에서와 같은 결과가 나오는걸 확인할 수 있다.

profile
백엔드 프로그래머

0개의 댓글