매일 배운 것을 이해한만큼 정리해봅니다.
오늘은 Head First Design Patterns 책을 읽으며 Strategy pattern과 Observer pattern을 공부해보았습니다.
프로그래밍 책은 궁금은 한데 쭉 진도 나가기가 어려워서(혹은 지겨워서) 요즘엔 3권 정도를 정해두고 틈이 될 때마다 1-2 챕터 정도를 돌려가면서 읽는 중이다. 그 중에 한 권이 'Head First Design Patterns'.
최근에 바닐라 자바스크립트로 상태관리 시스템을 만드는 블로그 글을 연달아 읽었는데 읽으면서 뭔가 내가 주로 쓰는 라이브러리들에서 자주본 것만 같은 구조적 모양새가 보였다. '그치, 상태관리 라이브러리 같은걸 막 만들진 않겠지. 뭔가 사람들이 잘 정립해둔 구조가 있을거야.' 싶은 마음에 자료를 찾다가 발견한 책이 이 책이다. 예전에 면접을 보면서 '디자인 패턴에 대해서 아는대로 얘기해보라.'는 답에 쩔쩔맸던 기억도 나고 그래서, 나중에 핵심만 보고 싶을 때를 위해서 글로 남겨보기로 했다.
(책의 예제 코드는 자바로 되어 있긴 한데, 글에서는 타입스크립트로 풀어 써보려고 한다.)
"누군가가 이미 여러분의 문제를 해결해 놓았습니다."
// ts file
// 유연하게 대응해야 하는 동작은 interface로 정의한다.
interface WeaponBehavior {
useWeapon: () => void;
}
// 구현: interface를 받아서 다양한 상세 동작을 정의해둔다.
class SwordBehaviors implements WeaponBehavior {
useWeapon() {
console.log("검을 휘두른다");
}
}
// 구현: interface를 받아서 다양한 상세 동작을 정의해둔다.
class KnifeBehaviors implements WeaponBehavior {
useWeapon() {
console.log("칼로 벤다");
}
}
// Character에는 WeaponBehavior가 있다: 추상 클래스를 정의한다.
abstract class Character {
weapon: WeaponBehavior;
constructor(weapon: WeaponBehavior) {
this.weapon = weapon;
}
abstract fight(): void;
setWeapon(weapon: WeaponBehavior) {
this.weapon = weapon;
}
}
// 상속: 캐릭터 추상 클래스를 상속하는 기사 클래스를 정의한다.
class Knight extends Character {
fight() {
this.weapon.useWeapon();
}
}
const knight = new Knight(new SwordBehaviors());
knight.fight();
// 콘솔로 출력: "검을 휘두른다"
// 클래스 바깥에서 변화하는 동작을 따로 관리한다: 무기를 변경한다.
knight.setWeapon(new KnifeBehaviors());
knight.fight();
// 콘솔로 출력: "칼로 벤다"
// ts file
interface Subject {
registerObserver: (obj: Observer) => void;
removeObserver: (obj: Observer) => void;
notifyObservers: () => void;
}
interface Observer {
/** Subject의 상태 변경 시 전달 받음 */
update: (observables: Record<string, any>) => void;
}
class WeatherData implements Subject {
observers: Observer[];
observables: Record<string, any>;
private changed: boolean = false;
constructor() {
this.observers = [];
this.observables = { temperature: 0, humidity: 0, pressure: 0 };
}
registerObserver(obj: Observer) {
console.log("옵저버를 등록", obj);
this.observers = [...this.observers, obj];
console.log("총 등록된 옵저버들", this.observers);
}
removeObserver(obj: Observer) {
console.log("옵저버를 제거", obj);
this.observers = this.observers.filter((observer) => observer !== obj);
console.log("남아 있는 옵저버들", this.observers);
}
notifyObservers() {
if (this.changed) {
this.observers.forEach((observer) => {
console.log("알림을 줄 옵저버", observer);
observer.update(this.observables);
});
}
this.changed = false;
}
private measurementsChanged() {
this.changed = true;
this.notifyObservers();
}
setMeasurements(values: {
temperature?: number;
humidity?: number;
pressure?: number;
}) {
this.observables = {
temperature: values.temperature || this.observables.temperature,
humidity: values.humidity || this.observables.humidity,
pressure: values.pressure || this.observables.pressure,
};
this.measurementsChanged();
}
subscribeSubject(observer: Observer) {
this.registerObserver(observer);
}
unsubscribeSubject(observer: Observer) {
this.removeObserver(observer);
}
}
class CurrentConditionObserver implements Observer {
update(observables: Record<string, any>) {
console.log("subject의 상태가 바뀌었다", observables);
}
}
const weatherData = new WeatherData();
const currentCondition = new CurrentConditionObserver();
weatherData.subscribeSubject(currentCondition);
// 콘솔로 출력: 옵저버를 등록 CurrentConditionObserver {}
// 콘솔로 출력: 총 등록된 옵저버들 [ CurrentConditionObserver {} ]
// 콘솔로 출력: 알림을 줄 옵저버 CurrentConditionObserver {}
weatherData.setMeasurements({ temperature: 20, humidity: 60, pressure: 10 });
// 콘솔로 출력: subject의 상태가 바뀌었다 { temperature: 20, humidity: 60, pressure: 10 }
weatherData.unsubscribeSubject(currentCondition);
// 콘솔로 출력: 옵저버를 제거 CurrentConditionObserver {}
// 콘솔로 출력: 남아 있는 옵저버들 []