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만큼만 점수를 출력한다.
Client는 ScoreRecord객체를 만들어 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클래스를 정의했다.
성적 변경의 통보수신이라는 측면에서 DataSheetView와 MinMaxView는 동일하므로 Subject클래스는 Observer인터페이스를 구현함으로써 성적 변경에 관심이 있음을 보여준다.
ScoreRecord의 addScore가 호출되면 자신의 성적 값을 저장한 후 Subject의 notifytObservers메소드를 호출해 두 클래스에게 성적 변경을 통보한다. 그러면 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);
}
}