🔩

NestJS: ゲッター/セッターの使う意味あるの?

に公開

はじめに

NestJS (およびTypeScript) でクラスを設計する際、ゲッターとセッターはオブジェクトのプロパティをカプセル化するための強力なツールです。しかし、その使い方を誤ると、かえってコードの可読性や保守性を損なう可能性もあります。

本記事では、NestJS開発の文脈で、ゲッターとセッターが本当に必要な場面、避けるべき場面、そして代替となるモダンなアプローチを、具体的なコードと共に解説します。

ゲッター/セッターが「不要」な場面(基本スタイル)

まず、NestJSにおいてゲッターやセッターが冗長になりがちな最も一般的なケースを見ていきます。多くの場合、よりシンプルで直接的な方法が推奨されます。

1. 依存性の注入 (DI)

DIコンテナからサービスを注入する場合、コンストラクタでprivate readonlyを使用するのが標準的なスタイルです。これは、依存性がクラス内部でのみ利用され、かつ不変であることを保証します。

@Controller('cats')
export class CatsController {
  // 依存性は外部から注入され、クラス内部で変更されるべきではない
  constructor(private readonly catsService: CatsService) {}
}

この場合、catsServiceは外部からアクセスされる必要も、内部で再代入される必要もないため、ゲッターもセッターも不要です。

2. DTOやエンティティ (データ保持オブジェクト)

APIのレスポンスやリクエストボディを定義するDTO (Data Transfer Object) では、各プロパティを明示的に定義するのがベストプラクティスです。

// DTO (Data Transfer Object) の正しい例
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, Min } from 'class-validator';

export class CatDto {
  @IsNumber()
  @ApiProperty({ description: '猫のID' })
  id: number;

  @IsString()
  @ApiProperty({ description: '猫の名前' })
  name: string;

  @IsNumber()
  @Min(0)
  @ApiProperty({ description: '猫の年齢' })
  age: number;
}

なぜこの書き方なのか?
コンストラクタでのショートハンド記法 (constructor(public readonly name: string)) は、コードが短くなる一方で、上記のような@ApiProperty@IsStringといったデコレータを付与できません。

NestJSでは、class-validatorによるバリデーションや@nestjs/swaggerによるAPIドキュメント生成がエコシステムの中心にあるため、プロパティを明示的に定義する方法が標準となります。

ゲッター (get) の有効な使い方

プロパティの値を返す際に、何らかの追加ロジックを加えたい場合にゲッターは非常に有効です。

ユースケース1: 派生データ (Computed Property)

複数のプロパティから計算して得られる値を、単一のプロパティとして公開したい場合です。

class User {
  public firstName: string;
  public lastName: string;

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

ユースケース2: 値のフォーマット変換

内部的なデータ表現と、外部に公開したい表現が異なるときに、その変換をゲッターに担わせます。

class Post {
  private _createdAt: Date;

  get formattedCreatedAt(): string {
    return this._createdAt.toLocaleDateString('ja-JP');
  }
}

セッター (set) の有効な使い方と注意点

セッターは値を設定する際にロジックを挟めるため強力ですが、使い方には注意が必要です。

注意点: 副作用を持つセッターはアンチパターン

まず、避けるべきパターンを見てみましょう。

// アンチパターンの例
class Circle {
  private _radius: number;
  public area: number; // 半径と連動して変わる

  set radius(value: number) {
    this._radius = value;
    this.area = Math.PI * value * value; // ★暗黙的な副作用
  }
}

radiusプロパティを設定すると、暗黙的にareaプロパティまで更新されてしまいます。このような副作用は、コードの振る舞いを予測しにくくし、デバッグを困難にするため、現代的な開発では避けられる傾向にあります。

推奨される代替アプローチ:
派生データは、ゲッターを使って必要な時に計算する方が安全です。

class Circle {
  public radius: number;

  get area(): number {
    // 面積は必要になった時点で計算して返す
    return this.calculateArea();
  }

  private calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

セッターの有効なユースケース

副作用を避けつつ、セッターが有効に機能する場面は以下の通りです。

1. 値のバリデーション

クラスの不整合な状態を防ぐため、プロパティに代入される値を検証します。これはセッターの最も正当な使い方の一つです。

class UserProfile {
  private _age: number;

  set age(value: number) {
    if (value < 0 || !Number.isInteger(value)) {
      throw new Error('年齢は0以上の整数である必要があります。');
    }
    this._age = value;
  }

  get age(): number {
    return this._age;
  }
}

2. 値の正規化

値を代入する前に、トリム(空白除去)や大文字/小文字変換などの正規化を行います。

class Account {
  private _email: string;

  set email(value: string) {
    this._email = value.toLowerCase().trim();
  }
}

まとめ

  • ゲッター/セッターが不要な場面: DIで注入された依存関係や、デコレータを多用するDTOなど。多くの場合、public readonlyprivate readonlyプロパティがより簡潔で安全です。

  • ゲッターが必要な場面: 派生データやフォーマット変換など、値を返す際に計算や加工が必要な場合

  • セッターが必要な場面: バリデーションや値の正規化など、値を設定する際に検証や変換のロジックが必要な場合

  • 避けるべきこと: セッターで他のプロパティの状態を暗黙的に変更するような副作用を起こすこと。

NestJS開発においては、フレームワークのエコシステム(DI、バリデーション、ドキュメンテーション)を最大限に活用するため、クラスのプロパティをどのように設計するかを意識することが、コードの品質を大きく左右します。

Discussion