You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

给装饰器类注入额外服务是否为反模式?Angular标题服务实现疑问

问题

我想在整个Angular应用中自定义Title类,为setTitle()添加扩展功能:部分组件直接调用@angular/platform的Title类setTitle方法,部分组件传入翻译键(如MY_USERS_PAGE_TITLE),获取翻译结果后设置标题。尝试继承Title类失败,因为它被标注为final类。于是我创建了可注入的装饰器类:

@Injectable({ providedIn: 'root' })
class TitleDecorator implements ITitle{
    protected title!: string;
    constructor(
        protected titleService: Title
    ) { }
    
    setTitle(newTitle: string): void {
        this.titleService.setTitle(newTitle);
    }
    getTitle(): string {
        return this.titleService.getTitle();
    }
    addTrailing(trailing: string): void {
        this.title += trailing;
        this.setTitle(this.title);
    }
}

并继承该类实现了支持翻译的TitleServiceTranslatable:

@Injectable({ providedIn: 'root' })
export class TitleServiceTranslatable extends TitleDecorator{
    constructor(
        private translateService: TranslateService,
        protected titleService: Title
    ) {
        super(titleService);
    }
    
    private subscriptions = new Subscription();
    setTitle(newTitle: string): void {
        this.subscriptions.add(
            this.translateService.get(newTitle).subscribe(title => {
                this.title = title;
                super.setTitle(this.title);
                this.subscriptions.unsubscribe();
            })
        );
    }
}

目前功能正常,但我有疑问:通过Angular DI自动注入额外服务后,装饰器理论上无法互相替换,这种实现方式是否属于反模式?恳请提供优化建议及更好的解决方案。


分析与优化建议

是否属于反模式

你的实现不算严格意义上的反模式,但存在依赖耦合过紧和扩展灵活性不足的问题:

  • TitleServiceTranslatable直接绑定TranslateService,导致它无法脱离翻译场景复用,也难以替换为其他扩展逻辑的装饰器
  • TitleDecorator未抽象出通用扩展点,后续新增功能(如标题前缀、动态拼接)只能通过继承叠加,容易形成臃肿的类层次结构

优化方案

方案1:DI替换原生Title服务(推荐)

既然原生Title是final类无法继承,直接在Angular DI容器中替换它的实现,让所有组件注入的Title都是自定义版本,同时兼容两种调用方式:

import { Inject, Injectable } from '@angular/core';
import { Title, DOCUMENT } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { catchError, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CustomTitleService extends Title {
  private currentTitle = '';

  constructor(
    @Inject(DOCUMENT) document: any, // 原生Title依赖的DOCUMENT
    private translateService: TranslateService
  ) {
    super(document);
  }

  setTitle(newTitle: string): void {
    // 优先尝试翻译,翻译失败或无匹配键时直接使用原字符串
    this.translateService.get(newTitle).pipe(
      catchError(() => of(newTitle))
    ).subscribe(translatedTitle => {
      this.currentTitle = translatedTitle;
      super.setTitle(this.currentTitle);
    });
  }

  addTrailing(trailing: string): void {
    this.currentTitle += trailing;
    super.setTitle(this.currentTitle);
  }
}

在AppModule的providers中替换原生服务:

import { Title } from '@angular/platform-browser';
import { CustomTitleService } from './custom-title.service';

@NgModule({
  providers: [
    { provide: Title, useClass: CustomTitleService }
  ]
})
export class AppModule { }

这样所有组件注入Title时,拿到的都是CustomTitleService,无需区分调用不同服务,直接传入字符串或翻译键即可。

方案2:策略模式分离扩展逻辑

如果需要支持多场景动态切换标题处理逻辑(如翻译、前缀拼接、格式化),可以用策略模式拆分功能:

  1. 定义策略接口:
import { Observable } from 'rxjs';

export interface TitleProcessingStrategy {
  process(title: string): Observable<string>;
}
  1. 实现翻译策略:
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { TitleProcessingStrategy } from './title-processing-strategy';
import { catchError, of } from 'rxjs';

@Injectable()
export class TranslationStrategy implements TitleProcessingStrategy {
  constructor(private translateService: TranslateService) {}

  process(title: string): Observable<string> {
    return this.translateService.get(title).pipe(
      catchError(() => of(title))
    );
  }
}
  1. 实现基础策略(直接返回原标题):
import { Injectable } from '@angular/core';
import { TitleProcessingStrategy } from './title-processing-strategy';
import { of } from 'rxjs';

@Injectable()
export class BasicStrategy implements TitleProcessingStrategy {
  process(title: string) {
    return of(title);
  }
}
  1. 实现策略驱动的自定义Title服务:
import { Inject, Injectable } from '@angular/core';
import { Title, DOCUMENT } from '@angular/platform-browser';
import { TitleProcessingStrategy } from './title-processing-strategy';
import { BasicStrategy } from './basic.strategy';

@Injectable({ providedIn: 'root' })
export class StrategyTitleService extends Title {
  private currentStrategy: TitleProcessingStrategy;
  private currentTitle = '';

  constructor(
    @Inject(DOCUMENT) document: any,
    private basicStrategy: BasicStrategy,
    private translationStrategy: TranslationStrategy
  ) {
    super(document);
    this.currentStrategy = this.basicStrategy; // 默认使用基础策略
  }

  // 动态切换策略
  useStrategy(strategy: TitleProcessingStrategy): void {
    this.currentStrategy = strategy;
  }

  setTitle(newTitle: string): void {
    this.currentStrategy.process(newTitle).subscribe(processedTitle => {
      this.currentTitle = processedTitle;
      super.setTitle(this.currentTitle);
    });
  }

  addTrailing(trailing: string): void {
    this.currentTitle += trailing;
    super.setTitle(this.currentTitle);
  }
}

这种方式的优势是:新增功能只需添加策略类,无需修改原有服务;可根据页面场景动态切换处理逻辑,符合单一职责原则。

现有代码的小修复

如果暂时不想重构,可先修复内存泄漏问题:

// TitleServiceTranslatable中修改订阅管理
private subscriptions = new Subscription();

setTitle(newTitle: string): void {
  const sub = this.translateService.get(newTitle).subscribe(title => {
    this.title = title;
    super.setTitle(this.title);
  });
  this.subscriptions.add(sub);
}

// 实现OnDestroy接口清理订阅
ngOnDestroy(): void {
  this.subscriptions.unsubscribe();
}

内容的提问来源于stack exchange,提问作者Fatih Ersoy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 02:54:35