RxJS 觀察者模式在 LanguageService 中的應用

概述

LanguageService 使用 RxJS 的觀察者模式來處理語言狀態變化,這是一個經典的發佈-訂閱模式實現。

核心概念

觀察者模式 (Observer Pattern)

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。當主題對象的狀態發生變化時,所有依賴於它的觀察者都會得到通知並自動更新。

RxJS Subject

RxJS 的 Subject 是一個特殊的 Observable,它可以同時作為 Observable 和 Observer: - 作為 Observable: 可以被訂閱 - 作為 Observer: 可以發送數據給訂閱者

LanguageService 中的實現

1. language$ = new ReplaySubject<LangChangeEvent>(1)

字段含義

  • language$: 命名慣例,$ 表示這是一個 Observable
  • ReplaySubject<LangChangeEvent>(1): 緩衝區大小為 1 的重播主體

ReplaySubject 特性

// ReplaySubject 會記住最後 N 個值
// 當有新的訂閱者時,會立即收到最近的 N 個值
language$ = new ReplaySubject<LangChangeEvent>(1);

為什麼使用 ReplaySubject?

  1. 狀態同步: 新組件訂閱時能立即獲取當前語言狀態
  2. 事件重播: 確保不會錯過語言變化事件
  3. 緩衝區控制: 只保留最新的語言狀態

2. 發佈事件

setLang(lang: string) {
  // 當語言改變時,發佈事件給所有訂閱者
  this.translateService.onLangChange.pipe(take(1)).subscribe(result => {
    this.language$.next(result);  // 發送事件
    localStorage.setItem(LanguageService.LANGUAGE, lang);
  });

  this.translateService.use(lang);
}

訂閱語法比較

語法 1: subscribe()

// 只關心事件發生,不處理數據
languageService.language$.subscribe();

// 相當於
languageService.language$.subscribe({
  next: () => {},      // 空處理函數
  error: () => {},     // 默認錯誤處理
  complete: () => {}   // 默認完成處理
});

使用場景

  • 只想觸發副作用(如日誌記錄)
  • 測試 Observable 是否正常工作
  • 簡單的事件監聽

語法 2: subscribe(event => { ... })

// 處理接收到的數據
languageService.language$.subscribe(event => {
  console.log('Language changed:', event.lang);
});

// 相當於
languageService.language$.subscribe({
  next: (event) => {
    console.log('Language changed:', event.lang);
  },
  error: (err) => console.error('Error:', err),
  complete: () => console.log('Completed')
});

使用場景

  • 需要處理事件數據
  • 執行業務邏輯
  • 更新 UI 狀態

完整訂閱對象語法

languageService.language$.subscribe({
  next: (event: LangChangeEvent) => {
    // 處理正常事件
    console.log('Language changed to:', event.lang);
    this.updateUI(event.lang);
  },
  error: (error: any) => {
    // 處理錯誤
    console.error('Language change error:', error);
  },
  complete: () => {
    // 處理完成(通常不會發生,因為 Subject 不會完成)
    console.log('Language monitoring completed');
  }
});

實際應用場景

組件中的使用

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    // 訂閱語言變化
    this.languageService.language$
      .pipe(takeUntil(this.destroy$))  // 防止記憶體洩漏
      .subscribe(event => {
        console.log('Language changed:', event.lang);
        // 更新組件狀態
        this.currentLang = event.lang;
        // 重新載入數據
        this.loadLocalizedData();
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

服務間通信

@Injectable()
export class TranslationService {
  constructor(private languageService: LanguageService) {
    // 監聽語言變化,重新載入翻譯
    this.languageService.language$.subscribe(event => {
      this.loadTranslations(event.lang);
    });
  }
}

ReplaySubject vs 其他 Subject

比較表

Subject 類型 緩衝區 新訂閱者行為 使用場景
Subject 只接收訂閱後的事件 簡單事件
BehaviorSubject 1 立即收到最新值 狀態管理
ReplaySubject N 收到最近 N 個值 事件歷史
AsyncSubject 1 只在完成時收到最後值 最終結果

LanguageService 為什麼選 ReplaySubject?

// BehaviorSubject 也可以,但 ReplaySubject 更靈活
language$ = new BehaviorSubject<LangChangeEvent | null>(null);

// ReplaySubject 允許更複雜的初始化邏輯
language$ = new ReplaySubject<LangChangeEvent>(1);

記憶體管理

重要提醒

// ✅ 正確:使用 takeUntil 防止洩漏
private destroy$ = new Subject<void>();

ngOnInit() {
  this.languageService.language$
    .pipe(takeUntil(this.destroy$))
    .subscribe(event => { ... });
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

// ❌ 錯誤:沒有取消訂閱會造成記憶體洩漏
ngOnInit() {
  this.languageService.language$.subscribe(event => { ... });
}

總結

language$ 的角色

  • 事件總線: 集中管理語言狀態變化
  • 狀態同步: 確保所有組件獲取最新語言狀態
  • 解耦通信: 服務和組件間的鬆耦合通信

訂閱語法選擇

  • subscribe(): 簡單事件觸發
  • subscribe(callback): 數據處理和業務邏輯
  • subscribe({next, error, complete}): 完整事件處理

這種設計實現了: - 響應式編程: 聲明式處理狀態變化 - 組件解耦: 服務和組件間鬆耦合 - 狀態一致性: 所有訂閱者同步更新 - 記憶體安全: 正確的訂閱管理 d:\wpg\shipment-notice-platform-web\observer-pattern-rxjs.md