この記事の目的
・個人的な備忘録
・コンテナプレゼンテーションパターンについて具体例を見ながら理解する
背景
私自身フロントエンド開発の経験はほとんどありませんでしたが、社内で使用する来客管理アプリの実装をフロントからバックエンドまで一人で設計、開発、テストまですることになり、フロントエンドの設計について調査していたところ、コンテナプレゼンテーションパターンというものを知り、社内アプリで使用するために調査しました。
フロントエンドの設計パターン
今回はフロントエンドにAngularを使用することにしたのですが、そもそもフロントエンド開発自体を経験したことがないのでコンポーネント?みたいなレベルで、そのレベルからコンポーネント設計、PJ全体の設計パターンなどを調査しました。ここらへんの記事が参考になったので同じような境遇の人はどうぞ!!
コンテナプレゼンテーションパターンとは
コンテナプレゼンテーションパターンの原則は以下です。
コンテナ
・データの取得や変更、状態管理
・APIの呼び出し、サービスの注入
・ルーティングや画面遷移
・状態に応じたプレゼンテーションへのデータの受け渡し
プレゼンテーション
・UIの描画
・ユーザの操作やイベントに対する応答
・コンテナから受け取ったデータを使用してUIの更新
(サービス)
・ビジネスロジック
前提としてコンポーネント設計を行う際には単一責任原則を守らなければなりません。一つのコンポーネントに対しては一つしか責務を負わせないというものです。この原則を守ることで、改修等が入るときに修正範囲が明確になり、改修の工数を削減できたり、意図しないバグを発生させないようにしたり、新たなメンバーが入ってきた時にコードリーディングしやすくなったりという様々なメリットを享受することができます。コンテナプレゼンテーションパターンでは、UIに関する処理を完全にプレゼンテーションに、ビジネスロジックはサービスクラスに、その他の処理はコンテナに任せます。そうすることでコンポーネントごとの責務がはっきりします。
コンテナプレゼンテーションパターンの具体例
今回私は、機能ごとにコンポーネントを分けたかったので、以下のようなディレクトリ構造にしました。例えばログイン処理についての処理を実装するなら、ログイン処理のUIに関する処理はlogin-presentation.componet.tsに、状態管理や画面遷移、ビジネスロジックの呼び出しはlogin-container.componet.tsに記述しました。
.
├── features
│ ├── login
│ │ ├──container
│ │ │ ├──login-container.componet.ts
│ │ │ ├──login-container.componet.spec.ts
│ │ │ ├──login-container.componet.css.ts
│ │ │ └──login-container.componet.html
│ │ │
│ │ ├──presentation
│ │ │ ├──login-presentation.componet.ts
│ │ │ ├──login-presentation.componet.spec.ts
│ │ │ ├──login-presentation.componet.css.ts
│ │ │ └──login-presentation.componet.html
ただ、調査してみると、このようにある機能に関するUI系の処理をプレゼンテーションに、その他をコンテナに分割するというよりも、最初から機能など気にせずにコンテナの役割を担うコンポーネント、プレゼンテーションの役割を担うコンポーネントを作成する方法が一般的らしいので王道のコンテナプレゼンテーションパターンのディレクトリ構造を知りたい方はこちらの記事を参考にしてください。
ログイン処理について記述します(Angularで記述しています)。
// login-container.component.ts
import { Component } from '@angular/core';
import { AuthService } from 'path-to-auth.service';
@Component({
selector: 'app-login-container',
template: `
<app-user-presentation
[isLoggedIn]="isLoggedIn"
[username]="username"
(loginSuccess)="handleLoginSuccess($event)"
(logoutClick)="logout()"
></app-user-presentation>
`,
})
export class LoginContainerComponent {
isLoggedIn: boolean = false;
username: string | null = null;
constructor(private authService: AuthService) {}
ngOnInit() {
this.checkLoginStatus();
}
//状態管理サービス呼び出し
checkLoginStatus() {
this.isLoggedIn = this.authService.isLoggedIn();
this.username = this.authService.getUsername();
}
//ログイン処理成功時のビジネスロジック呼び出し
handleLoginSuccess(username: string) {
this.checkLoginStatus();
// ログイン成功後の他の処理を追加(例: 画面遷移など)
}
logout() {
this.authService.logout();
this.checkLoginStatus();
}
}
コンテナでは状態管理、サービスに記述されたビジネスロジックの呼び出し、プレゼンテーションに表示データを受け渡すことに専念し、HTMLはシンプルになっています。
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-presentation',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" *ngIf="!isLoggedIn; else userDetails">
<label>
Username:
<input type="text" formControlName="username" />
<div *ngIf="username.invalid && (username.dirty || username.touched)">
<div *ngIf="username.errors.required">Username is required.</div>
</div>
</label>
<br />
<label>
Password:
<input type="password" formControlName="password" />
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<div *ngIf="password.errors.required">Password is required.</div>
</div>
</label>
<br />
<button type="submit" [disabled]="userForm.invalid">Login</button>
</form>
<ng-template #userDetails>
<div>
<h1>Welcome, {{ username }}!</h1>
<button (click)="logout()">Logout</button>
</div>
</ng-template>
`,
})
export class UserPresentationComponent {
@Input() isLoggedIn: boolean = false;
@Input() username: string | null = null;
@Output() loginSuccess = new EventEmitter<string>();
@Output() logoutClick = new EventEmitter<void>();
userForm: FormGroup;
constructor(private fb: FormBuilder) {
//フォームのバリデーション定義
this.userForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
});
}
get username() {
return this.userForm.get('username');
}
get password() {
return this.userForm.get('password');
}
//ログインボタンクリック時の処理
//親コンポーネントであるlogin-containerにフォームの中身を送信している
onSubmit(): void {
if (this.userForm.valid) {
this.loginSuccess.emit(this.username.value);
}
}
logout(): void {
this.logoutClick.emit();
}
}
プレゼンテーションではフォームとバリデーションの定義、コンテナから受け取ったデータの表示、ユーザの入力内容をコンテナに送信する役割をになっています。
参考サイト