Skip to content

Commit

Permalink
[오혜성] 챕터 7: 자바스크립트 디자인 패턴 (3/3) (#66)
Browse files Browse the repository at this point in the history
* 7-3 1부

* 7-3 2부
  • Loading branch information
hyesungoh authored Nov 14, 2024
1 parent add5e7a commit 5d9084c
Showing 1 changed file with 387 additions and 0 deletions.
387 changes: 387 additions & 0 deletions 챕터_7/오혜성.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,5 +440,392 @@ class Circle extends Shape {
}
```

## 플라이웨이트 패턴

```ts
// 플라이웨이트 패턴 예시 - 폰트 스타일 공유하기

// 공유할 폰트 스타일 (내부 상태)
interface FontStyle {
name: string;
size: number;
color: string;
}

// 플라이웨이트 팩토리
class FontStyleFactory {
private static fontStyles: { [key: string]: FontStyle } = {};

static getFontStyle(name: string, size: number, color: string): FontStyle {
// 캐시 키 생성
const key = `${name}-${size}-${color}`;

// 이미 존재하는 스타일이면 재사용
if (!this.fontStyles[key]) {
console.log('새로운 폰트 스타일 생성:', key);
this.fontStyles[key] = { name, size, color };
}

return this.fontStyles[key];
}
}

// 텍스트 클래스 (외부 상태)
class Text {
private content: string;
private style: FontStyle;

constructor(content: string, name: string, size: number, color: string) {
this.content = content;
// 플라이웨이트 패턴 사용 - 스타일 공유
this.style = FontStyleFactory.getFontStyle(name, size, color);
}

render(): void {
console.log(`텍스트: ${this.content}`);
console.log(`폰트: ${this.style.name}, 크기: ${this.style.size}, 색상: ${this.style.color}`);
}
}

// 사용 예시
const text1 = new Text("안녕하세요!", "Arial", 12, "black");
const text2 = new Text("반갑습니다!", "Arial", 12, "black"); // 동일한 스타일은 재사용됨
const text3 = new Text("좋은 하루!", "Arial", 14, "blue"); // 새로운 스타일 생성

text1.render();
text2.render();
text3.render();

```

- 경량화를 위한 패턴

> 이벤트 위임과 비교해서 설명하는데, 잘 공감되지는 않았음
> 이라 썻다가 쓰면서
> 이벤트 위임을 경량화 관점에서 볼 수도 있겠지?
> 라고 생각이 들었는데, 이걸 말하고 싶었던 것일라나 ;
## 행위 패턴

- 객체 간의 의사소통을 돕는 패턴
- 관찰자
- 중재자
- 커맨드

## 관찰자 패턴

```ts
// 관찰자 인터페이스
interface Observer {
update(message: string): void;
}

// 주체(Subject) 클래스
class NewsAgency {
private observers: Observer[] = [];
private news: string = "";

// 관찰자 등록
subscribe(observer: Observer): void {
this.observers.push(observer);
}

// 관찰자 제거
unsubscribe(observer: Observer): void {
this.observers = this.observers.filter(obs => obs !== observer);
}

// 모든 관찰자에게 알림
notifyObservers(): void {
this.observers.forEach(observer => observer.update(this.news));
}

// 새로운 뉴스 설정
setNews(news: string): void {
this.news = news;
this.notifyObservers();
}
}

// 구체적인 관찰자 클래스
class NewsSubscriber implements Observer {
private name: string;

constructor(name: string) {
this.name = name;
}

update(news: string): void {
console.log(`${this.name}님이 새로운 뉴스를 받았습니다: ${news}`);
}
}

// 사용 예시
const newsAgency = new NewsAgency();
const subscriber1 = new NewsSubscriber("홍길동");
const subscriber2 = new NewsSubscriber("김철수");

newsAgency.subscribe(subscriber1);
newsAgency.subscribe(subscriber2);

newsAgency.setNews("오늘은 날씨가 맑습니다!");
// 출력:
// 홍길동님이 새로운 뉴스를 받았습니다: 오늘은 날씨가 맑습니다!
// 김철수님이 새로운 뉴스를 받았습니다: 오늘은 날씨가 맑습니다!

```

- 그냥 옵저버 걸고, 걸어진 객체들한테 다 쏘는 ㅇㅇ

## 발행/구독 패턴

- 관찰자 패턴에 더해서 토픽/이벤트 채널을 둔 형태
- 특화된 이벤트를 정의할 수 있고
- 구독자에게 필요한 값이 포함된 커스텀 인자를 전달할 수도 있고
- 결국 '발행자와 구독자를 각자 독립적으로 유지'
- 느슨한 결합

```js
const events = () => {
const topics = {};
const hOP = topics.hasOwnProperty;

// ...
if (!hOP.call(topics, topic)) {}
// 이렇게 하는 이유가 있을까?
if (topics.hasOwnProperty(topic)) {}
// 이거랑 다른게 뭐지?
}

// 그래서 제 친구한테 물어봄

// 1. 안정성
const topics = {
hasOwnProperty: () => {}
}
// 이런 경우가 있을 수 있어서 ㅋ 라네요

// 2. 성능 최적화
// hOP 형태처럼 할당해두면 프로토타입 체인 탐색을 줄일 수 있어서 ㅇㅇ

// 근데 난 선호되지 않는 형태 같음
```

```ts
// 발행/구독 패턴 예시
class EventEmitter {
private subscribers: { [key: string]: Function[] } = {};

// 구독하기
subscribe(eventName: string, callback: Function) {
if (!this.subscribers[eventName]) {
this.subscribers[eventName] = [];
}
this.subscribers[eventName].push(callback);

// 구독 취소 함수 반환
return () => {
this.subscribers[eventName] = this.subscribers[eventName].filter(
cb => cb !== callback
);
};
}

// 이벤트 발행하기
publish(eventName: string, data?: any) {
if (!this.subscribers[eventName]) {
return;
}

this.subscribers[eventName].forEach(callback => callback(data));
}
}

// 사용 예시
const emitter = new EventEmitter();

// 날씨 이벤트 구독
const unsubscribeWeather = emitter.subscribe('weather', (data) => {
console.log(`날씨 업데이트: ${data}`);
});

// 뉴스 이벤트 구독
const unsubscribeNews = emitter.subscribe('news', (data) => {
console.log(`뉴스 업데이트: ${data}`);
});

// 이벤트 발행
emitter.publish('weather', '맑음'); // 출력: 날씨 업데이트: 맑음
emitter.publish('news', '새로운 소식입니다!'); // 출력: 뉴스 업데이트: 새로운 소식입니다!

// 구독 취소
unsubscribeWeather();
emitter.publish('weather', ''); // 아무것도 출력되지 않음
```

- 애플리케이션을 더 작고 느슨하게 연결된 부분으로 나눌 수 있음
- 결과적으로 코드의 관리와 재사용성을 높임
- 데이터를 어떻게 처리할지는 각 구독자가 정함

- 발행자는 시스템의 연결이 분리된 특성 때문에 구독자 기능이 제대로 동작하지 않아도 이를 알 수 없음
- 구독자들이 서로의 존재에 대해 알 수 없음
- 발행자를 변경하는 데 드는 비용을 파악할 수 없음

- 발행 구독 패턴은 자바스크립트 생태계와 매우 잘 어울리는데, ECMAScript의 구현체가 본질적으로 이벤트 기반이기 때문

> 이 부분을 공감할 정도로 이해가 깊지 않은 거 같음
> 커스텀 이벤트 dispatch 정도밖에 떠오르지 않음
- RxJS 이야기
- 뭐 예제를 찔끔 넣어둬서 먼 말인지

## 중재자 패턴

- 중앙 집중식 통제가 핵심
- 사례로 이벤트 위임을 들 수 있음

- 관찰자, 발행/구독 패턴과의 차이점
- 중재자는 자신이 보유한 정보를 바타으로 각 객체의 메서드 호출 시점과 업데이트의 필요성을 판단
- 이를 통해 워크플로와 프로세스를 캡슐화하고 여러 객체 사이를 조율
- 상호 연관성을 갖는 변화가 존재할 경우 중재자 패턴을 사용하여 구현하는 것이 적합

```ts
// 중재자 패턴 예시 - 채팅방

// 중재자 인터페이스
interface ChatMediator {
sendMessage(message: string, sender: User): void;
addUser(user: User): void;
}

// 구체적인 중재자
class ChatRoom implements ChatMediator {
private users: User[] = [];

addUser(user: User): void {
this.users.push(user);
console.log(`${user.getName()}님이 채팅방에 입장했습니다.`);
}

sendMessage(message: string, sender: User): void {
// 메시지를 보낸 사용자를 제외한 모든 사용자에게 메시지 전달
this.users
.filter(user => user !== sender)
.forEach(user => user.receive(message, sender.getName()));
}
}

// 사용자 클래스
class User {
private name: string;
private mediator: ChatMediator;

constructor(name: string, mediator: ChatMediator) {
this.name = name;
this.mediator = mediator;
this.mediator.addUser(this);
}

getName(): string {
return this.name;
}

send(message: string): void {
console.log(`${this.name}: ${message}`);
this.mediator.sendMessage(message, this);
}

receive(message: string, sender: string): void {
console.log(`${this.name}이(가) ${sender}로부터 메시지를 받음: ${message}`);
}
}

// 사용 예시
const chatRoom = new ChatRoom();
const user1 = new User("철수", chatRoom);
const user2 = new User("영희", chatRoom);
const user3 = new User("민수", chatRoom);

user1.send("안녕하세요!");
user2.send("반갑습니다!");

```

## 커맨드 패턴

- 명령을 실행하는 객체와 명령을 호출하는 객체 간의 결합을 느슨하게 해 구체적인 클래스의 변경에 대한 유연성을 향상
- 기본 원칙이 명령을 내리는 객체와 명령을 실행하는 객체의 책임을 분리하는 것

- 책의 예제가 너무 빈약해 손으로는 이해가 안됨

```ts
// 커맨드 인터페이스
interface Command {
execute(): void;
}

// 구체적인 커맨드 클래스들
class LightOnCommand implements Command {
private light: Light;

constructor(light: Light) {
this.light = light;
}

execute(): void {
this.light.turnOn();
}
}

class LightOffCommand implements Command {
private light: Light;

constructor(light: Light) {
this.light = light;
}

execute(): void {
this.light.turnOff();
}
}

// 리시버(Receiver) - 실제 동작을 수행하는 객체
class Light {
turnOn(): void {
console.log("불이 켜졌습니다.");
}

turnOff(): void {
console.log("불이 꺼졌습니다.");
}
}

// 인보커(Invoker) - 커맨드를 실행하는 객체
class RemoteControl {
private command: Command;

setCommand(command: Command): void {
this.command = command;
}

pressButton(): void {
this.command.execute();
}
}

// 사용 예시
const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);

const remote = new RemoteControl();

// 불 켜기
remote.setCommand(lightOn);
remote.pressButton();

// 불 끄기
remote.setCommand(lightOff);
remote.pressButton();

```

0 comments on commit 5d9084c

Please sign in to comment.