목차

Observer pattern

🗓️

Observer 패턴

  • Oberver패턴은 데이터의 변경이 발생했을 경우 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보하고자 할 때 유용하다.
  • 예를 들어 새로운 파일이 추가되거나 기존 파일이 삭제되었을 때 탐색기는 이를 즉시 표시할 필요가 있다.
  • 다른 예로는 차량의 연료가 소진될 떄 까지 주행 가능 거리를 출력하는 클래스, 연료량이 부족하면 메시지를 보내는 클래스 등이 있다.

성적 출력 툴 만들기

  • 입력된 성적 값을 출력하는 프로그램을 만들어 보자.
  • 입력된 점수를 저장하는 ScoreRecord클래스와 점수를 목록 형태로 출력하는 DataSheetView클래스가 필요하다.
public class ScoreRecord {

    private List<Integer> scores = new ArrayList<Integer>();
    private DataSheetView dataSheetView;

    public void setDataSheetView(DataSheetView dataSheetView) {
        this.dataSheetView = dataSheetView;
    }

    public void addScore(int score) {
        scores.add(score);
        dataSheetView.update();
    }

    public List<Integer> getScoreRecord() {
        return scores;
    }
}

public class DataSheetView {

    private ScoreRecord scoreRecord;
    private int viewCount;

    public DataSheetView(ScoreRecord scoreRecord, int viewCount) {
        this.scoreRecord = scoreRecord;
        this.viewCount = viewCount;
    }

    public void update() {
        List<Integer> record = scoreRecord.getScoreRecord();
        displayScores(record, viewCount);
    }

    public void displayScores(List<Integer> record, int viewCount) {
        System.out.print("List of " + viewCount + " entries:");
        int i = 0;
        while (!record.isEmpty()) {
            if (viewCount == i || i >= record.size()) {
                break;
            }
            System.out.print(record.get(i) + " ");
            i += 1;
        }
        System.out.println(" ");
    }

}

public class Client {

    public static void main(String[] args) {
        ScoreRecord scoreRecord = new ScoreRecord();
        DataSheetView dataSheetView = new DataSheetView(scoreRecord, 3);

        scoreRecord.setDataSheetView(dataSheetView);
        scoreRecord.addScore(10);
        scoreRecord.addScore(20);
        scoreRecord.addScore(30);
        scoreRecord.addScore(40);
        scoreRecord.addScore(50);
    }

}
  • 점수의 추가인 addScore 메소드가 호출되면 ScoreRecord클래스는 자신의 필드인 socres에 점수를 추가한다. 그리고 DataSheetView클래스의 update메소드를 호출해 성적을 출력하도록 한다.
  • DataSheetView는 정해진 viewCount만큼만 점수를 출력한다.
  • ClientScoreRecord객체를 만들어 DataSheetView 생성자에 실어보낸다. setDataSheetView를 통해 update메소드를 호출할 DataSheetView인스턴스를 실어보낸다.
  • 내상각엔 Client에서 인스턴스를 저렇게 실어보내는것 자체가 이상한것 같다.. as-is예시가 조금 그렇네

문제점

  • 성적을 다른 형태로 출력하고 싶다면 어떤 변경 작업을 해야 하는가? (ex 최대/최소)
  • 여러 가지 형태의 성적을 동시 혹은 순차적으로 출력하려면 어떤 변경 작업을 가져야 하는가?

성적을 다른 형태로 출력

  • 점수가 입력되면 점수 목록을 출력하는 대신 최대/최소 값만 출력하려면 기존의 DataSheetView클래스 대신 최대최소 값을 출력하는 MinMaxView를 추가한다.
    • 이 경우에는 OCP에 위배된다. 점수가 입력되었을때 지정된 특정 대상 클래스에게 고정적으로 통보하도록 코딩이 되었는데 다른 대상 클래스에게 점수가 입력되었음을 통보하려면 ScoreRecord클래스의 변경이 불가피하기 때문이다.

동시 혹은 순차적 성적 출력

  • 성적이 입력 되었을 때 최대 3개 목록, 최대 5개 목록, 최대최소 값을 동시에 출력하거나 처음에는 목록으로 출력하고 나중에는 최대최소 값을 출력하려면 어떻게 해야할까? 실제 출력 기능을 고려하기 전에 점수가 입력되면 복수개의 대상 클래스를 갱신하는 구조를 먼저 생각해봐야 한다.
  • 목록으로 출력하는것은 DataSheetView를 활용하고 최대최소는 MinMaxView를 활용한다. 그러므로 ScoreRecord는 2개의 DataSheetView 객체와 1개의 MinMaxView객체에게 성적 추가 통보를 해야한다.
    • 이렇게 변경되는 경우에도 다른 기능을 추가할때마다 ScoreBoard는 계속 변경되어야 한다.

해결책

  • 문제의 핵심은 성적 통보 대상이 변경되더라도 ScoreRecord클래스가 재사용 되지 않고 계속 변경된다는 점이다. 따라서 ScoreRecord에서 변화되는 부분을 식별하고 이를 일반화 해야한다.
  • ScoreRecord는 통보 대상인 객체를 참조하는 것으로 관리해야 한다.
  • addScore메소드는 각 통보 대상인 객체의 update메소드를 호출할 필요가 있다.
  • 공통 기능을 상위 클래스 및 인터페이스로 일반화 하고 이를 활용해 ScoreRecord를 구현하는 방식으로 설계를 변경하자.
  • 다이어그램을 살펴보면 성적 변경 관심사의 객체관리 기능이 구현된 Subject클래스를 정의했다.
  • 성적 변경의 통보수신이라는 측면에서 DataSheetViewMinMaxView는 동일하므로 Subject클래스는 Observer인터페이스를 구현함으로써 성적 변경에 관심이 있음을 보여준다.
  • ScoreRecordaddScore가 호출되면 자신의 성적 값을 저장한 후 SubjectnotifytObservers메소드를 호출해 두 클래스에게 성적 변경을 통보한다. 그러면 Subject클래스는 Observer인터페이스를 통해 두 클래스의 Update메소드를 호출한다.
  • 아래는 UML을 구현한 코드다.
public interface Observer {

    public abstract void update();

}

public abstract class Subject {

    private List<Observer> observers = new ArrayList<Observer>();

    public void attace(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer o : observers) {
            o.update();
        }
    }

}

public class ScoreRecord extends Subject {

    private List<Integer> scores = new ArrayList<Integer>();


    public void addScore(int score) {
        scores.add(score);
        notifyObservers();
    }

    public List<Integer> getScoreRecord() {
        return scores;
    }


}

public class DataSheetView implements Observer {

    private ScoreRecord scoreRecord;
    private int viewCount;

    public DataSheetView(ScoreRecord scoreRecord, int viewCount) {
        this.scoreRecord = scoreRecord;
        this.viewCount = viewCount;
    }

    public void update() {
        List<Integer> record = scoreRecord.getScoreRecord();
        displayScores(record, viewCount);
    }

    public void displayScores(List<Integer> record, int viewCount) {
        System.out.print("List of " + viewCount + " entries:");
        int i = 0;
        while (!record.isEmpty()) {
            if (viewCount == i || i >= record.size()) {
                break;
            }
            System.out.print(record.get(i) + " ");
            i += 1;
        }
        System.out.println(" ");
    }

}

public class MinMaxView implements Observer {

    private ScoreRecord scoreRecord;

    public MinMaxView(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }

    public void update() {
        List<Integer> record = scoreRecord.getScoreRecord();
        displayMinMax(record);
    }

    private void displayMinMax(List<Integer> record) {
        int min = Collections.min(record, null);
        int max = Collections.max(record, null);
        System.out.println("Min: " + min + " / Max: " + max);
    }

}

public class Client {

    public static void main(String[] args) {

        ScoreRecord scoreRecord = new ScoreRecord();
        DataSheetView dataSheetView3 = new DataSheetView(scoreRecord, 3);
        DataSheetView dataSheetView5 = new DataSheetView(scoreRecord, 5);
        MinMaxView minMaxView = new MinMaxView(scoreRecord);

        scoreRecord.attace(dataSheetView3);
        scoreRecord.attace(dataSheetView5);
        scoreRecord.attace(minMaxView);

        scoreRecord.addScore(10);
        scoreRecord.addScore(20);
        scoreRecord.addScore(30);
        scoreRecord.addScore(40);
        scoreRecord.addScore(50);
    }

}
  • 성적 변경에 관심있는 객체들의 관리는 Subject에서 구현하고 ScoreRecord 클래스는 Subject클래스를 상속받게 함으로써 ScoreRecord클래스는 이제 두 클래스를 직접 참조할 필요가 없게 됐다.
  • 그러므로 ScoreRecord클래스의 코드를 변경하지 않고도 새로운 관심 클래스 및 객체를 추가/제거 하는것이 가능해졌다.

추가기능 붙여보기

  • 평균/합계를 출력하는 StatisticsView기능을 만들어 붙여보자.
public class StatisticsView implements Observer {

    private ScoreRecord scoreRecord;

    public StatisticsView(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }

    @Override
    public void update() {
        List<Integer> record = scoreRecord.getScoreRecord();
        displayStatistics(record);
    }

    private void displayStatistics(List<Integer> record) {
        int sum = 0;
        for (int score : record) {
            sum += score;
            float average = (float) sum / record.size();
            System.out.println("Sum: " + sum + " / Average: " + average);
        }
    }
}

public class Client {

    public static void main(String[] args) {

        ScoreRecord scoreRecord = new ScoreRecord();
        DataSheetView dataSheetView3 = new DataSheetView(scoreRecord, 3);
        DataSheetView dataSheetView5 = new DataSheetView(scoreRecord, 5);
        MinMaxView minMaxView = new MinMaxView(scoreRecord);

        StatisticsView statisticsView = new StatisticsView(scoreRecord);

        scoreRecord.attach(dataSheetView3);
        scoreRecord.attach(dataSheetView5);
        scoreRecord.attach(minMaxView);
        scoreRecord.attach(statisticsView);

        scoreRecord.addScore(10);
        scoreRecord.addScore(20);
        scoreRecord.addScore(30);
        scoreRecord.addScore(40);
        scoreRecord.addScore(50);
    }
}