Angular 15 專案的觀察者模式工具選擇

專案版本分析

根據 package.json 的依賴分析:

{
  "@angular/core": "^15.2.0",
  "rxjs": "~7.8.0",
  "typescript": "~4.9.4"
}

結論:Angular 15 無法使用 Signals,RxJS 是最佳選擇

為什麼 RxJS 是最佳選擇

1. 🎯 版本兼容性

  • Angular 15: ✅ 完美支持 RxJS
  • Signals: ❌ 需要 Angular 17+
  • RxJS 生態: ✅ Angular 內建依賴

2. 🏗️ 專案規模適配

這個**企業級物流平台**具有以下特點:

複雜的狀態管理需求

// 多語言狀態管理
language$ = new ReplaySubject<LangChangeEvent>(1);

// 用戶認證狀態
// 業務數據流
// API 響應處理

異步操作豐富

  • HTTP 請求處理
  • 文件上傳/下載
  • 實時數據更新
  • 表單驗證響應

3. 🔧 Angular 內建集成

框架級支持

// HttpClient 返回 Observable
this.http.get('/api/data').subscribe(data => { ... });

// Router 事件
this.router.events.pipe(filter(event => event instanceof NavigationEnd))

// Form 狀態變化
this.form.valueChanges.subscribe(values => { ... });

依賴注入系統

@Injectable({
  providedIn: 'root'
})
export class DataService {
  data$ = new BehaviorSubject<any[]>([]);
  // 服務間通信
}

RxJS 在這個專案中的應用場景

1. 語言狀態管理 (當前實現)

// ✅ 當前使用 ReplaySubject - 適合的選擇
language$ = new ReplaySubject<LangChangeEvent>(1);

2. API 數據流

// 貨物數據流
shipments$ = this.http.get<Shipment[]>('/api/shipments');

// ASN 狀態更新
asnStatus$ = new BehaviorSubject<AsnStatus>('pending');

3. 組件間通信

// 通知組件刷新
refresh$ = new Subject<void>();

// 觸發刷新
this.refresh$.next();

4. 複雜業務邏輯

// 組合多個數據流
customerOrders$ = combineLatest([
  this.customerService.selectedCustomer$,
  this.orderService.allOrders$
]).pipe(
  map(([customer, orders]) =>
    orders.filter(order => order.customerId === customer.id)
  )
);

替代方案評估

不推薦:Angular Signals

// 需要 Angular 17+,無法使用
private language = signal<string>('en-us'); // ❌ 編譯錯誤

⚠️ 有限適用:EventEmitter

// 只適合簡單場景
languageChanged = new EventEmitter<string>();

// 缺點:
- 無法組合操作
- 沒有重播機制
- 記憶體洩漏風險高

不推薦:原生 CustomEvent

// 過於基礎,缺乏類型安全和錯誤處理
window.dispatchEvent(new CustomEvent('languageChange', { detail: lang }));

RxJS Subject 類型選擇指南

適用於這個專案的 Subject 類型

Subject 類型 使用場景 當前使用
ReplaySubject ✅ 語言狀態 (需要重播最新狀態) language$
BehaviorSubject ✅ 用戶狀態、配置數據 推薦用於業務數據
Subject ✅ 事件通知、一次性事件 適合按鈕點擊等
AsyncSubject ⚠️ 最終結果 較少使用

具體改進建議

1. 優化當前實現

@Injectable({
  providedIn: 'root'
})
export class LanguageService {
  // ✅ 保持 ReplaySubject,但添加類型安全
  private languageSubject = new ReplaySubject<LangChangeEvent>(1);
  readonly language$ = this.languageSubject.asObservable();

  // 添加當前語言的 BehaviorSubject
  private currentLangSubject = new BehaviorSubject<string>('en-us');
  readonly currentLanguage$ = this.currentLangSubject.asObservable();

  setLang(lang: string) {
    this.translateService.use(lang);
    this.currentLangSubject.next(lang);
  }
}

2. 添加業務數據流

@Injectable({
  providedIn: 'root'
})
export class ShipmentService {
  // ASN 數據流
  private asnSubject = new BehaviorSubject<AsnData | null>(null);
  readonly asn$ = this.asnSubject.asObservable();

  // 發票數據流
  private invoicesSubject = new BehaviorSubject<Invoice[]>([]);
  readonly invoices$ = this.invoicesSubject.asObservable();
}

3. 使用 RxJS 操作符

// 組合數據流
customerShipments$ = combineLatest([
  this.customerService.selectedCustomer$,
  this.shipmentService.shipments$
]).pipe(
  map(([customer, shipments]) =>
    shipments.filter(s => s.customerId === customer.id)
  ),
  shareReplay(1) // 共享訂閱
);

性能和維護建議

最佳實踐

  • 使用 asObservable() 暴露只讀流
  • 正確取消訂閱防止記憶體洩漏
  • 使用 shareReplay() 共享熱流
  • 合理選擇 Subject 類型

⚠️ 避免的問題

// ❌ 錯誤:暴露 Subject 允許外部 next()
public language$ = new ReplaySubject<LangChangeEvent>(1);

// ✅ 正確:只暴露 Observable
private languageSubject = new ReplaySubject<LangChangeEvent>(1);
public language$ = this.languageSubject.asObservable();

暴露 Subject 的嚴重缺點

1. 封裝性完全破壞
// 任何地方都可以隨意發送語言事件
languageService.language$.next({ lang: 'fake-lang' } as LangChangeEvent);
languageService.language$.next({ lang: 'invalid-lang' } as LangChangeEvent);
languageService.language$.complete(); // 甚至可以結束流
languageService.language$.error(new Error('外部錯誤')); // 發送錯誤
2. 狀態管理混亂
  • 多個來源: 任何組件都可以"假裝"是語言服務發送事件
  • 不可預測: 很難追蹤狀態變化來自哪裡
  • 競爭條件: 多個地方同時修改狀態導致衝突
3. 調試和維護困難
// 當出現問題時,很難找到是誰發送了錯誤事件
languageService.language$.subscribe(event => {
  console.log('收到語言事件:', event);
  // 可能是真正的語言切換,也可能是外部惡意發送
});
4. 測試變得複雜
it('應該處理語言變化', () => {
  // 測試時外部代碼可能會隨意發送事件
  languageService.language$.next({ lang: 'test-lang' });
  // 測試結果不可預測
});
5. API 設計違反原則
  • 單一責任: Subject 同時負責接收和發送
  • 接口汙染: 暴露了不應該暴露的內部實現
  • 契約破壞: 破壞了"只讀"的隱含契約

真實世界的危害示例

// 在某個組件中
@Component({...})
export class SomeComponent {
  constructor(private languageService: LanguageService) {}

  ngOnInit() {
    // ❌ 錯誤:以為只是訂閱,實際上可以修改
    // 開發者以為這只是訂閱,實際上可以隨意發送事件
    this.languageService.language$.next({ lang: 'debug-mode' });

    // ❌ 或者更糟:可以結束整個流
    this.languageService.language$.complete();
    // 現在整個應用都收不到語言變化事件了!
  }
}

正確的使用方式說明

在組件中,languageService.language$Observable<LangChangeEvent>,你只能執行以下操作:

1. subscribe() - 基本訂閱(較少使用)
// 只觸發執行,不處理數據
languageService.language$.subscribe();
// 等同於:subscribe(() => {}, () => {}, () => {})
// 用途:只是想確認 Observable 有在發送事件,常用於調試
2. subscribe(next) - 處理數據
// ✅ 最常用的形式
languageService.language$.subscribe((event: LangChangeEvent) => {
  console.log('語言改變:', event.lang);
  this.currentLang = event.lang;
  this.updateUI(event.lang);
});
3. subscribe(next, error) - 處理數據和錯誤
languageService.language$.subscribe(
  (event: LangChangeEvent) => {
    console.log('語言改變:', event.lang);
  },
  (error: any) => {
    console.error('語言服務錯誤:', error);
  }
);
4. subscribe(next, error, complete) - 完整處理
languageService.language$.subscribe(
  (event: LangChangeEvent) => {
    console.log('語言改變:', event.lang);
  },
  (error: any) => {
    console.error('語言服務錯誤:', error);
  },
  () => {
    console.log('語言監聽已完成');
  }
);

🔄 觀察者模式數據流詳細說明

subscribe() 中的數據來源不是從 "observe" 定義來的,而是從 Observable 的數據生產者發送的。

數據流工作原理

  1. 數據生產者: LanguageService 內部的 ReplaySubject
  2. 數據發送: 通過 next() 方法發送數據
  3. 數據接收: 訂閱者通過 subscribe() 接收數據

具體數據流示例

// 在 LanguageService 中
setLang(lang: string) {
  // 第1步:調用 ngx-translate 的 use() 方法
  this.translateService.use(lang);
  
  // 第2步:訂閱 ngx-translate 的語言變化事件
  this.translateService.onLangChange.pipe(take(1)).subscribe(result => {
    // 第3步:將收到的 LangChangeEvent 重新發送給我們的訂閱者
    this.language$.next(result); // ← 這裡是數據的來源!
  });
}

// 在組件中
languageService.language$.subscribe(
  (event: LangChangeEvent) => {
    // 第4步:組件收到 LanguageService 發送的 LangChangeEvent
    console.log('語言改變:', event.lang); // event 就是來自第3步的 result
  }
);

數據來源追蹤

原始來源: ngx-translate 的 TranslateService.onLangChange
↓
中間處理: LanguageService.setLang() 方法
↓
最終發送: this.language$.next(result)
↓
接收者: 組件的 subscribe() 回調函數

數據類型說明

  • LangChangeEvent: 來自 @ngx-translate/core 的接口
  • 數據內容: { lang: string, translations?: any }
  • 發送時機: 每次調用 setLang()
  • 接收時機: 所有訂閱了 language$ 的觀察者都會收到
5. subscribe(observer) - 傳入觀察者對象
languageService.language$.subscribe({
  next: (event: LangChangeEvent) => {
    console.log('語言改變:', event.lang);
  },
  error: (error: any) => {
    console.error('錯誤:', error);
  },
  complete: () => {
    console.log('完成');
  }
});

📋 LangChangeEvent 資料來源說明

LangChangeEvent 是從 @ngx-translate/core 套件匯入的 Angular 官方多語言翻譯套件類型:

import { LangChangeEvent, TranslateService } from '@ngx-translate/core';

LangChangeEvent 接口結構

interface LangChangeEvent {
  lang: string;        // 當前語言代碼,如 'en-us', 'zh-tw', 'zh-cn'
  translations?: any;  // 可選的翻譯數據
}

TranslateService.use(lang) 被調用時,ngx-translate 會發射 LangChangeEvent 事件,包含新的語言代碼。

你不能執行的操作

// ❌ 這些操作都會編譯錯誤,因為 language$ 是 Observable,不是 Subject
languageService.language$.next(event);        // 錯誤
languageService.language$.complete();         // 錯誤
languageService.language$.error(error);       // 錯誤

// ❌ 無法直接賦值
languageService.language$ = new Subject();    // 錯誤

Observable vs Subject 的區別

操作 Observable (language$) Subject (如果暴露)
subscribe()
pipe()
next()
complete()
error()

實際應用示例

@Component({...})
export class LanguageIndicatorComponent {
  currentLang = 'en-us';

  constructor(private languageService: LanguageService) {}

  ngOnInit() {
    // ✅ 正確:只能訂閱
    this.languageService.language$.subscribe({
      next: (event) => {
        this.currentLang = event.lang;
        this.updateDisplay();
      },
      error: (error) => {
        console.error('語言載入錯誤:', error);
        this.showErrorMessage();
      }
    });
  }

  // 如果要改變語言,必須調用服務的方法
  changeLanguage(lang: string) {
    this.languageService.setLang(lang); // ✅ 正確
  }
}

正確的封裝方式

@Injectable({
  providedIn: 'root'
})
export class LanguageService {
  // 私有 Subject - 只有服務內部可以控制
  private languageSubject = new ReplaySubject<LangChangeEvent>(1);

  // 公開只讀 Observable - 外部只能訂閱
  public language$ = this.languageSubject.asObservable();

  // 明確的公開方法 - 控制狀態變化的唯一途徑
  setLang(lang: string) {
    // 內部邏輯驗證
    if (!this.isValidLanguage(lang)) {
      throw new Error(`無效的語言: ${lang}`);
    }

    // 只有這裡可以發送事件
    this.translateService.use(lang);
    this.languageSubject.next({ lang } as LangChangeEvent);
  }

  private isValidLanguage(lang: string): boolean {
    return this.languageList.includes(lang);
  }
}

封裝帶來的優勢

  1. 可控性: 只有服務決定什麼時候發送什麼事件
  2. 驗證: 可以驗證輸入數據的有效性
  3. 一致性: 確保所有事件都遵循相同格式
  4. 調試友好: 所有狀態變化都有明確的來源
  5. 測試容易: 可以輕鬆 mock 和驗證行為

總結

🎯 最終建議

對於 Angular 15.2.0 的物流管理平台:

  1. 繼續使用 RxJS - 它是最佳和最成熟的選擇
  2. 優化 Subject 使用 - 正確選擇 Subject 類型
  3. 遵循觀察者模式最佳實踐 - 封裝狀態,暴露只讀流
  4. 考慮未來遷移 - 為將來升級到 Angular 17+ 做準備

RxJS 提供了這個專案需要的全部功能:狀態管理、異步處理、數據流組合,以及與 Angular 生態的完美集成。 d:\wpg\shipment-notice-platform-web\angular-15-observer-pattern.md