1-1節では、複数の責任を持ったコンポーネントをリファクタリングし、メンテナンスしやすい単一責任のコンポーネントへと分割を進めるパターンを学びます。
コンポーネントの責任
現在のAppComponent
は次のようになっています。
<ul>
<li *ngFor="let user of users">
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
users: User[] = [];
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get<{ data: User[] }>('https://reqres.in/api/users').subscribe(resp => {
this.users = resp.data;
});
}
}
今のAppComponent
は次のようなたくさんの責任を負っています。
- アプリケーションブートストラップに使われるルートコンポーネントとなること
- ユーザーの配列をAPIから取得すること
- ユーザーの配列をリストとして表示すること
- ユーザーの情報を表示すること
これは単一責任原則に反しています。このままでは、AppComponent
はいくつもの理由で変更されることになります。1つめのルートコンポーネントとしての役割は捨てることができないものですが、それ以外の責任はそれぞれを担当するコンポーネントに分割することができます。
まずはリストで反復される単位要素を分割することで、ひとつの責任を別のモジュールに切り出すことにしましょう。
UserListItemComponent
の分割
ユーザーリストの繰り返し単位のコンポーネントを UserListItemComponent
として分割します。このコンポーネントの責任は、渡された1件のユーザーをリストアイテムとして表示することです。それ以外の理由でこのコンポーネントが変更されてはいけません。
コンポーネントの分割は次のような手順で行います。
- 新しいコンポーネントのファイルを作成する(
ng g component
) - 元のコンポーネントのテンプレートから新しいコンポーネントに分割したい範囲を移植する
- 新しいコンポーネントに必要なプロパティやメソッドを定義する
- 元のコンポーネントのテンプレートを新しいコンポーネントで置き換える
UserListItemComponent
のために切り出されるテンプレートは次の範囲です。
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}
このテンプレートに必要な user
プロパティを定義し、親コンポーネントからデータを受け取るためにインプットとして機能するようにします。
UserListItemComponent
は次のようになります。
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { User } from '../../user';
@Component({
selector: 'user-list-item',
templateUrl: './user-list-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListItemComponent {
@Input()
user!: User;
}
次のようにAppComponent
のテンプレートの一部をUserListItemComponent
で置き換えます。これでAppComponent
はユーザー情報の表示に関する責任から開放されました。たとえばユーザーのメールアドレスを表示することになっても、AppComponent
を変更する必要はありません。
<ul>
<li *ngFor="let user of users">
<user-list-item [user]="user"></user-list-item>
</li>
</ul>
UserListComponent
の分割
続いてもうひとつの責任、ユーザーの配列をどのようにリストとして表示するかという関心事をAppComponent
から切り出すことにしましょう。同じように新しく UserListComponent
を作成し、テンプレートを切り出します。
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { User } from '../../user';
@Component({
selector: 'user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
@Input()
users!: User[];
}
<ul>
<li *ngFor="let user of users">
<user-list-item [user]="user"></user-list-item>
</li>
</ul>
結果として、AppComponent
のテンプレートは次のようになります。AppComponent
が持つ責任は、アプリケーションのルートコンポーネントであることと、ユーザーリストのデータを取得することになりました。ユーザーリストをどのように表示するかというUIの関心事はUserListComponent
とUserListItemComponent
に分離できました。
次はユーザーリストのデータを取得する責任を移譲するため、UIとは関係のないビジネスロジックをサービスを使ってリファクタリングしましょう
<user-list [users]="users"></user-list>
ビジネスロジックの分離
ユーザーリストのデータを取得する責任はさらに2つの責任に分解できます。すなわち、HTTPクライアントを使ってユーザーリストを取得することと、取得したデータを UserListComponent
に渡すために保持することです。まずはHTTPクライアントを使うビジネスロジックを切り出すことで、1つ目の責任を移譲することにしましょう。
Angularの一般的なアプローチとして、UIに関係しないビジネスロジックはコンポーネントから サービス に分離されます。Angularにおけるサービスという語彙は、特定の実装やインターフェースの名称ではなく、ある単一の関心のために作られた、依存性の注入により利用されるクラス全般を指します。
HTTPクライアントでユーザーリストを取得する処理をUserService
というサービスに移譲します。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '../user';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) { }
getUsers(): Observable<User[]> {
return this.http
.get<{ data: User[] }>("https://reqres.in/api/users")
.pipe(
map(resp => resp.data)
);
}
}
AppComponent
のコンストラクタでUserService
を注入して getUsers
メソッドを呼び出します。
HTTPクライアントを利用する処理をAppComponent
から隠蔽することで、URLの変更やHTTPヘッダの追加のためにAppComponent
が変更されることがなくなります。
import { Component } from '@angular/core';
import { User } from './user';
import { UserService } from './service/user.service';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
users: User[] = [];
constructor(private userService: UserService) { }
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
}
AppComponent
の責務がどんどん減ってきましたね。あともう少しです。
ユーザーリストの状態管理
ユーザーリストを表示するための配列はAppComponent
のクラスプロパティとして保持されています。この責任を UserService
へ移譲しましょう。移譲するにあたり、UserService
を次のように整理します。
-
get users$(): Observable<User[]>
: ユーザーリストのデータをObservableとして返すgetterメソッド -
fetchUsers(): void
ユーザーリストのデータの取得を開始するメソッド
このように、副作用を起こさず値を返す クエリ と、副作用を起こし結果を返さない コマンド のメソッドを分離する考え方をコマンド・クエリ分離原則といいます。現在の getUsers()
メソッドはHTTPリクエストを発行し、表示するユーザーリストを変更させる副作用がある一方で、呼び出し元に結果も返しています。これを分離し、HTTPリクエストを発行してアプリケーションの状態を書き換えるコマンドと、アプリケーションの状態を監視するクエリを作成します。
今回のリファクタリングでUserService
はRxJSの BehaviorSubject
を使ってユーザーの配列を保持します。BehaviorSubject
はObservable
であると同時にSubject
であるため、そのまま外部に露出すると UserService
の外から値を書き込むことができてしまいます。そのためユーザーリストの状態はプライベートフィールドで保持し、getterを使って Observable
としてのインターフェースだけを公開します。
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { BehaviorSubject } from "rxjs";
import { map } from "rxjs/operators";
import { User } from "../user";
@Injectable({ providedIn: "root" })
export class UserService {
private usersSubject = new BehaviorSubject<User[]>([]);
get users$() {
return this.usersSubject.asObservable();
}
constructor(private http: HttpClient) {}
fetchUsers(): void {
this.http
.get<{ data: User[] }>("https://reqres.in/api/users")
.pipe(map(resp => resp.data))
.subscribe(users => {
this.usersSubject.next(users);
});
}
}
このサービスをAppComponent
から利用すると次のようになります。AppComponent
が直接データを保持することをやめて、UserService
が提供するusers$
配列をUIに反映することだけが責任になっています。
ユーザーの配列が同期的な配列からObservableに変わったので、テンプレート中でAsyncパイプを使って購読することを忘れないようにしましょう。
import { Component } from '@angular/core';
import { UserService } from './service/user.service';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
users$ = this.userService.users$;
constructor(private userService: UserService) { }
ngOnInit() {
this.userService.fetchUsers();
}
}
<user-list *ngIf="users$ | async as users" [users]="users"></user-list>
ついに、AppComponent
からほとんどの責任を移譲することができました。今のAppComponent
に残っているのはルートコンポーネントであることと、アプリケーションの起動時にユーザーリストの取得を開始することだけです。
ところで、今のAppComponen
tとUserService
の関係を図にすると次のようになります。よくみるとこれはビューとストア、アクションによる単方向のデータフローを構成しており、簡易的ではありますがFluxに近い実装となっています。コンポーネントが状態の変更を待ち受け、コンポーネントからのアクションから発生する副作用により状態が更新されるという設計パターンは、Angularに限らず昨今のコンポーネント中心のアプリケーション開発において欠かせない考え方のひとつです。
アプリケーションが現在管理する状態はユーザーリストの配列だけですが、機能が増えてアプリケーションの状態が複雑になるに従って、UserService
だけで管理することが単一責任原則に反していきます。
次のページからは少し複雑性が高くなっていくなかでのアプリケーション設計を学びましょう。