observer 패턴은 통보 대상 클래스와 통보 하는 클래스 간의 의존성을 제거한다.
Context
모니터링 툴이나 통계 툴은 원본데이터가 변할 때마다 항상 그래프나 그리드에 해당 데이터를 반영하여 관제자가 볼 수 있도록 한다.
이제부터 학생의 성적에 관한 모니터링 툴을 만들 것이다. 일반적으로 생각했을 때는 아래와 같이 ScoreRecord
와 DataSheetView
를 서로 의존하여
ScoreRecord
의 addScore
와 같은 점수 변경, 추가, 삭제에 대한 메서드라 호출될 때 DataSheetView
의 객체에 변동되었다고 통보(update
)해주면 된다.
먼저, DataSheetView
클래스는 ScoreRecord
속성으로 가지고 있는데, 그냥 update(ScoreRecord:ScoreRecord)
형식으로 실행하면 되는 것이 더 쉽지 않을까
생각할 수 있다. 보통 모니터링 툴 속의 특정 통계 그래프를 관찰하면 해당 통계(View)는 바라보고 있는 특정한 데이터의 집합이 있다. 따라서 통계뷰는 객체의 단위이며 특정
데이터 레코드에 대해서 의존관계를 가지고 있으므로 필드로 가지고 있어야한다.
만약 update(ScoreRecord:ScoreRecord)
형식의 메서드라면 유틸성 메서드가 되므로 인스턴스 메서드보단 정적 메서드로 존재해야한다.
정적메서드로 존재하게 된다면 업데이트의 대상이 자기 자신인지 알 수 없다.
class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
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;
}
}
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);
}
private void displayScores(List<Integer> record, int viewCount) {
System.out.print("List of " + viewCount + " entries: ");
for (int i = 0; i < viewCount && i < record.size(); i++) {
System.out.print(record.get(i) + " ");
}
System.out.println();
}
}
Problem
변경에 대해서 안전한가
일단 바로 생각해봐도 View를 교체하는 것에 대해서 안전하지 않다. OCP를 만족할 수 없다. 구현한 View 클래스에 대해서 직접 의존하고 있기 때문에
RecordSocre
의 수정은 불가피하다.
확장에 대해서 안전한가
기존의 DataSheetView
뿐만 아니라 다른 뷰도 추가하고 싶다. 일단 먼저 MinMaxView
를 추가해보자.
class MinMaxView {
private ScoreRecord scoreRecord;
public MinMaxView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}
public void update() {
displayMinMax(scoreRecord.getScoreRecord());
}
private void displayMinMax(List<Integer> scores) {
int max = Collections.max(scores, null);
int min = Collections.min(scores, null);
System.out.println("Min Max : " + min + ", " + max);
}
}
그리고 ScoreRecord
의 객체의 데이터가 변경될 때 역시 View에 통보를 해줘야하므로 ScoreRecord
에 필드에 추가하고 addScore
에 update
함수를 호출한다.
class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
private DataSheetView dataSheetView;
private MinMaxView minMaxView; // 추가
public void setDataSheetView(DataSheetView dataSheetView) {
this.dataSheetView = dataSheetView;
}
public void setMinMaxView(MinMaxView minMaxView) {
this.minMaxView = minMaxView;
}
public void addScore(int score) {
scores.add(score);
dataSheetView.update();
minMaxView.update(); // 추가
}
public List<Integer> getScoreRecord() {
return scores;
}
}
기능의 확장에 대해서 ScoreRecord
를 수정했으므로 OCP에 위반한다, 확장에 대해서 안전하지 않다.
동일 클래스의 대상이 2개 이상의 경우에도 안전한가
정책이 다른 동일 타입의 View를 추가하고 싶다. 그러면 복수의 같은 인스턴스를 가지게 되므로 아래와 같이 변경해야한다. 그리고 새로운 View 추가될 수 록 변경해야하는 요소가 많아진다, MinMaxView 역시 복수개가 존재하게 된다면 변경을 해야한다.
class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
private List<DataSheetView> dataSheetViews = new ArrayList<>();
private MinMaxView minMaxView;
public void addDataSheetView(DataSheetView dataSheetView) {
dataSheetViews.add(dataSheetView);
}
public void setMinMaxView(MinMaxView minMaxView) {
this.minMaxView = minMaxView;
}
public void addScore(int score) {
scores.add(score);
dataSheetViews.forEach(DataSheetView::update);
minMaxView.update();
}
public List<Integer> getScoreRecord() {
return scores;
}
}
Solution
위와 같은 문제는 우선적으로 View의 구현클래스에 직접적으로 ScoreRecord
가 관계를 가지고 있어서 발생하는 문제이다. 다른 패턴처럼 ScoreRecord
의
변화의 부분에 대해서 일반화 시키면 ScoreRecord
는 일반화된 인터페이스를 의존하게 되고, 기능이 확장되더라고 기존 코드에는 아무런 수정을 하지 않는다.
먼저 데이터의 변경에 대해서 관찰하고 있는 클래스를 일반화하면 아래와 같다. 각 View
들은 모두 update
라는 공통적인 메서드을 가지고 있다.
interface Observer {
public void update();
}
그리고 아래와 같이 구현한다.
class DataSheetView implements Observer {
private ScoreRecord scoreRecord;
private int count;
public DataSheetView(ScoreRecord scoreRecord, int count) {
this.scoreRecord = scoreRecord;
this.count = count;
}
@Override
public void update() {
displayDataSheet(scoreRecord.getScores());
}
public void displayDataSheet(List<Integer> scores) {
StringBuffer stringBuffer = new StringBuffer("List of Scores : ");
for(int i = 0; i < count && i < scores.size(); i++) {
stringBuffer.append(scores.get(i) + " " );
}
System.out.println(stringBuffer.toString());
}
}
class MinMaxView implements Observer {
private ScoreRecord scoreRecord;
public MinMaxView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}
@Override
public void update() {
displayMinMax(scoreRecord.getScores());
}
public void displayMinMax(List<Integer> scores) {
System.out.println("Min : " + Collections.min(scores) + ", Max : " + Collections.max(scores));
}
}
통보 대상을 관리하고 통보시키는 역할을 추상화할 수 있다. 관리자는 Observer
를 자신의 것으로 종속시킬 수 있기에 이름이 Subject
로 붙은 것 같다.
abstract class Subject {
private List<Observer> observers = new ArrayList<>();
public void attachObserver(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
protected void notifyObserver() {
observers.forEach(Observer::update);
}
}
Observer
들을 붙히거나(attach) 때거나(detach) 이들에 변경에 대해서 통보(notifyObserver)한다. 하지만 Subject
는 스스로 아무런 상태를 가지고 있지 않기 때문에
추상클래스로 인스턴스를 생성할 수 없게 한다.
class ScoreRecord extends Subject {
private List<Integer> scores = new ArrayList<>();
public void addScore(int score) {
scores.add(score);
notifyObserver();
}
public List<Integer> getScores() {
return scores;
}
}
ScoreRecord
는 Subject
를 상속받음으로서 스스로 Observer
들을 관리할 수 있는 관리자가 된다. 그럼 여기서 하나의 View를 추가해보자.
class StatisticsView implements Observer{
private ScoreRecord scoreRecord;
public StatisticsView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}
@Override
public void update() {
displayStatistics(scoreRecord.getScores());
}
private void displayStatistics(List<Integer> scores) {
scores.stream().reduce(Integer::sum).ifPresent((totalValue) -> {
System.out.println("Sum : " + totalValue + ", Average : " + totalValue/scores.size());
});
}
}
그리고 이를 아래와 같이 사용할 수 있다.
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
scoreRecord.attachObserver(new StatisticsView(scoreRecord));
for(int i = 0; i < 10; i ++) {
scoreRecord.addScore((int)(Math.random()*100));
}
}
기능을 확장했음에도 ScoreRecord
는 아무런 변화가 없었다. 즉 OCP를 만족한다. 이를 다이어그램으로 간단히 표현하면 아래와 같다.
Summary
옵저버 패턴은 통보 대상 객체의 관리를 Subject
클래스와 Observer
인터페이스로 일반화하여, 실제 ConcreteSubject
와
ConcreteObserver
클래스 간의 의존성을 제거한다. ConcreteObserver
클래스의 파생클래스가 얼마든지 생겨도
결과적으로 ConcreteSubject
코드는 변화가 없다.
역할
- Observer : 데이터의 변경을 통보받는 인터페이스.
- Subject : ConcreteObserver 객체를 관리하는 관리자
- ConcreteSubject : 변경 관리 대상이 되는 데이터를 가지고 있는 클래스
- ConcreteObserver : ConcreteSubject로부터 변경된 데이터를 통보받는 클래스
'소프트웨어 > 디자인패턴' 카테고리의 다른 글
[소프트웨어/디자인패턴] 데커레이터 패턴(Decorator Pattern) (0) | 2017.01.25 |
---|---|
[소프트웨어] 디자인 패턴에 대해서 (0) | 2017.01.23 |
[소프트웨어/디자인패턴] 커맨드 패턴(Command Pattern) (2) | 2017.01.21 |
[소프트웨어/디자인패턴] 스테이트 패턴(State Pattern) (0) | 2017.01.19 |
[소프트웨어/디자인패턴] 싱글톤 패턴(Singleton Pattern) (1) | 2017.01.19 |