iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
1
Modern Web

Angular 全集中筆記系列 第 20

第 20 型 - 依賴注入 (Dependency Injection, DI)

  • 分享至 

  • xImage
  •  

在上兩篇中,建立了兩個服務,並利用依賴注入 (Dependency Injection, DI) 的方法注入至元件內;這一篇將進一步了解 Angular 的依賴注入 (Dependency Injection, DI) 框架的使用方法。

配置可注入的服務

Angular 在啟動的過程,會自動為每個模組建立注入器 (injecor),來提供依賴對象的單一實體。從 TaskRemoteService 中,可以看到此服務中使用了 @Injectable 裝飾器,透過此裝飾器裝服務實體註冊到根注入器 (AppModule) 中。

@Injectable({
  providedIn: "root",
})
export class TaskRemoteService {}

@Injectable 裝飾器是 Angular 6 所新增的,透過此裝飾器的定義,Angular 在編譯時會將未使用的依賴對象搖樹優化 (Tree-Shaking) 而排除;而在 Angular 6 之前則會定義在 @NgModule 裝飾器的提供者 (provider) 陣列內,在此定義的依賴對象則皆會被封裝至模型內。

@NgModule({
  imports: [CommonModule, HttpClientModule],
  declarations: [
    TaskComponent,
    TaskStateColorDirective,
    TaskListComponent,
    TaiwanDatePipe,
  ],
  providers: [TaskRemoteService],
  exports: [TaskComponent, TaskListComponent],
})
export class TaskModule {}

獨體設計模式 (Singleton) 的服務實體

透過 @Injectable 裝飾器將服務實體註冊到注入器中,而在此注入器的範圍內,所註冊的服務實體最多只會有一個,即多個元件所注入的特定服務,皆是使用同一個實體。為了解此獨體模式 (Singleton),建立一個計數器服務 (CounterService),並在此服加入新增與取得方法。

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root",
})
export class CounterService {
  count = 0;

  constructor() {}

  add(): void {
    this.count++;
  }
}

接著分別在 AppComponent 與 TaskListComponent 注入此計數器服務,並在頁面分別加入累加與顯示計數功能。

export class AppComponent implements OnInit {
  constructor(public counterService: CounterService) {}
}
<div>
  <strong>AppComponent</strong>
  <button type="button" (click)="counterService.add()">
    Add {{ counterService.count }}
  </button>
</div>

由結果可知,在 AppComponent 或 TaskListComponent 增加計數值,皆會連動至其他的數值顯示。

Couter

在 Angular 框架中,有兩個注入器層次結構,第一種稱為 ModuleInjector,可以利用 @Injectable 裝飾器或是定義 @NgModule()providers 陣列進行配置;另一種是 Angular 為每一個 DOM 元素隱含建立的 ElementInjector,可以利用 @Component 裝飾器中的 providers 屬性來配置服務。再加上,Angular 會依注入器為範圍來建立依賴實體,所以如果需要讓 AppComponent 與 TaskListComponent 各自的計數不同,可以在 TaskListComponent 中加入提供者 (providers) 的設定。

@Component({
  selector: "app-task-list",
  templateUrl: "./task-list.component.html",
  styleUrls: ["./task-list.component.css"],
  providers: [CounterService],
})
export class TaskListComponent implements OnInit {
  tasks$: Observable<Task[]>;

  constructor(
    private taskService: TaskRemoteService,
    public counterService: CounterService
  ) {}

  ngOnInit(): void {
    this.tasks$ = this.taskService.getData();
  }
}

Counter

利用依賴的提供者抽換服務

前一篇在變更待辦事項的服務,是直接修改 task-list.component.ts 中的建構式注入。此一做法違反了 SOLID 原則中的開放封閉原則 (Open-Closed Principle, OCP),若在較複雜的應用程式中可能會發生改 A 壞 B 的狀況,或者需要進行較大範圍的測試;而在 Angular 的 DI 框架中,可以利用提供者的設定來抽換服務。

SOLID 原則是在物件導向設計中,增加軟體維護性的五個原則,其包含:

  • 單一職責原則 (Single Responsibility Principle, SRP)
  • 開放封閉原則 (Open-Closed Principle, OCP)
  • 里氏替換原則 (Liskov Substitution Principle, LSP)
  • 介面隔離原則 (Interface Segergation Principle, ISP)
  • 依賴反轉原則 (Dependency Inversion Principle, DIP)

當若要將 TaskRemoteService 修改為 TaskLocalService 時,首先需要讓兩者的方法介面相同,因此修改 task-local.service.ts 檔案,把 getData() 方法回傳變更成 Observable 物件。

import { Injectable } from "@angular/core";
import { Observable, of } from "rxjs";

import { TaskState } from "../../enum/task-state.enum";
import { Task } from "../../model/task";

@Injectable({
  providedIn: "root",
})
export class TaskLocalService {
  private _tasks: Task[];

  getData(): Observable<Task[]> {
    console.log("from TaskLocalService");
    return of(this._tasks);
  }
}

最後,只要在 app.module.ts 檔案變更 TaskRemoteService 的提供者,就可以直接抽換掉待辦事項服務。

@NgModule({
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    TaskModule,
    UiModule,
  ],
  declarations: [AppComponent],
  providers: [{ provide: TaskRemoteService, useClass: TaskLocalService }],
  bootstrap: [AppComponent],
})
export class AppModule {}

Provider Class

除了指定類別來替代服務,Angular 也允許指定物件值來做抽換。

@NgModule({
  providers: [
    {
      provide: TaskRemoteService,
      useValue: {
        getData: () => {
          console.log("from value provider");
          return of([]);
        },
      },
    },
  ],
})
export class AppModule {}

Provider Value

在實務中對於相同的服務,可能會同時有新舊版本的程式實作,且希望能將新版程式取代舊版程式,此時若使用 useClass 時,Angular 會針對新舊版本各自建立實體;使用 useExisting 可以來讓 Angular 使用已建立的實體取代服務。

@NgModule({
  providers: [
    TaskLocalService,
    { provide: TaskRemoteService, useExisting: TaskLocalService },
  ],
})
export class AppModule {}

結論

這一篇進一步說明了 Angular 的 DI 框架基本運作觀念,善加利用依賴注入 (Dependency Injection, DI) 可以讓應用程式能加的容易維護。


上一篇
第 19 型 - HttpClient
下一篇
第 21 型 - 範本驅動表單 (Template-Driven Form)
系列文
Angular 全集中筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言