在上兩篇中,建立了兩個服務,並利用依賴注入 (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 {}
透過 @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 增加計數值,皆會連動至其他的數值顯示。
在 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();
}
}
前一篇在變更待辦事項的服務,是直接修改 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 {}
除了指定類別來替代服務,Angular 也允許指定物件值來做抽換。
@NgModule({
providers: [
{
provide: TaskRemoteService,
useValue: {
getData: () => {
console.log("from value provider");
return of([]);
},
},
},
],
})
export class AppModule {}
在實務中對於相同的服務,可能會同時有新舊版本的程式實作,且希望能將新版程式取代舊版程式,此時若使用 useClass
時,Angular 會針對新舊版本各自建立實體;使用 useExisting
可以來讓 Angular 使用已建立的實體取代服務。
@NgModule({
providers: [
TaskLocalService,
{ provide: TaskRemoteService, useExisting: TaskLocalService },
],
})
export class AppModule {}
這一篇進一步說明了 Angular 的 DI 框架基本運作觀念,善加利用依賴注入 (Dependency Injection, DI) 可以讓應用程式能加的容易維護。