1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】リファクタリングを通して学ぶドメイン駆動設計 Part4: ユースケースの作成

Last updated at Posted at 2025-12-04

はじめに

この記事は、以下の記事の続きです。

今回はユースケースの作成について説明します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12

ユースケースとは

ユースケースは、アプリケーション層に配置される、システムの具体的な操作を表すクラスです。ビジネスフローを明確にし、ドメイン層が定めたルールに従って、インフラ層に仕事をさせる役割を持ちます。

特徴 説明
ビジネスフローの実行 1つのユースケースが1つのビジネスフローを表現します。 ユーザー作成など
ドメイン層の協調 エンティティや値オブジェクトを組み合わせて、ビジネスルールを実行します。 メール重複チェック → エンティティ生成 → 保存
トランザクション境界 ユースケースがトランザクションの境界となることが多いです。 ユースケース実行中はDB操作を1つのトランザクションで管理
インフラからの独立 HTTPやデータベースなどの技術的詳細に依存しません CLIからもWebからも同じユースケースを利用可能

ユーザー作成ユースケース

ユーザー作成ユースケースを実装します。

application/usecases/CreateUserUseCase.ts
import { IUserRepository } from "../../domain/repositories/IUserRepository";
import { Email } from "../../domain/value-objects/Email";
import { UserName } from "../../domain/value-objects/UserName";
import { User } from "../../domain/entities/User";

export interface CreateUserInput {
  email: string;
  name: string;
}

export interface CreateUserOutput {
  id: number;
  email: string;
  name: string;
}

export class CreateUserUseCase {
  constructor(private readonly userRepository: IUserRepository) {}

  async execute(input: CreateUserInput): Promise<CreateUserOutput> {
    // 1. 値オブジェクトの生成
    const email = new Email(input.email);
    const name = new UserName(input.name);

    // 2. ビジネスルール: メールアドレスの重複チェック
    const exists = await this.userRepository.existsByEmail(email);
    if (exists) {
      throw new Error("Email already exists");
    }

    // 3. エンティティの生成
    const user = User.create(email, name);

    // 4. 永続化
    const savedUser = await this.userRepository.save(user);

    // 5. 結果の返却
    return savedUser.toObject();
  }
}

ユースケースの構造

ユースケースは以下の構造になっています。

1. コンストラクタでの依存性注入

constructor(private readonly userRepository: IUserRepository) {}

リポジトリインターフェースを受け取ることで、具体的なデータベース実装に依存しません。

依存性(Dependency)とは

クラスがその仕事を完遂するために「必要とする他のクラスやサービス」のことです。
CreateUserUseCase は、メールアドレスの重複チェックやデータ永続化のため、 IUserRepository に依存しています。

注入(Injection)とは

依存性をクラスの内部で勝手に作らず、外部から渡して(注入して)あげることです。

  • 注入がない場合(自己生成):
class BadUseCase {
  private userRepository: PrismaUserRepository; // ❌ 自分で具体的なDB実装を作成

  constructor() {
    this.userRepository = new PrismaUserRepository();
  }
  // ...
}

この場合、BadUseCasePrismaUserRepository という具体的な実装に強く依存してしまいます。

  • 注入がある場合(外部から渡す):
class CreateUserUseCase {
  // ✅ 依存物(IUserRepository)を外部から受け取る
  constructor(private readonly userRepository: IUserRepository) {}
  // ...
}

この設計手法は、DDDの目的である「依存性逆転の原則 (Dependency Inversion Principle: DIP)」を実現する核となります。

ユースケースは、具体的なデータベースの技術(Prisma)ではなく、抽象的なインターフェース(IUserRepository)に依存しているため、データベースを自由に入れ替えられる柔軟性とテストのしやすさを獲得できます。

2. executeメソッド

async execute(input: CreateUserInput): Promise<CreateUserOutput>

ユースケースの実行は必ず execute メソッドで行います。入力と出力の型を明確にすることで、使い方が分かりやすくなります。

3. ビジネスフローの実装

// 1. 値オブジェクトの生成
const email = new Email(input.email);
const name = new UserName(input.name);

2. ビジネスルール: メールアドレスの重複チェック
const exists = await this.userRepository.existsByEmail(email);
if (exists) {
  throw new Error('Email already exists');
}

// 3. エンティティの生成
const user = User.create(email, name);

// 4. 永続化
const savedUser = await this.userRepository.save(user);

// 5. 結果の返却
return savedUser.toObject();

このように、ユースケースを見れば「ユーザー作成の手順」が明確に理解できます。

同様に、他のユースケースも実装します。

ユーザー取得ユースケース

application/usecases/GetUserUseCase.ts
import { IUserRepository } from "../../domain/repositories/IUserRepository";
import { UserId } from "../../domain/value-objects/UserId";

export interface GetUserInput {
  id: number;
}

export interface GetUserOutput {
  id: number;
  email: string;
  name: string;
}

export class GetUserUseCase {
  constructor(private readonly userRepository: IUserRepository) {}

  async execute(input: GetUserInput): Promise<GetUserOutput | null> {
    const userId = new UserId(input.id);
    const user = await this.userRepository.findById(userId);

    if (!user) return null;

    return user.toObject();
  }
}

全ユーザー取得ユースケース

application/usecases/GetAllUsersUseCase.ts
import { IUserRepository } from "../../domain/repositories/IUserRepository";

export interface GetAllUsersOutput {
  users: {
    id: number;
    email: string;
    name: string;
  }[];
}

export class GetAllUsersUseCase {
  constructor(private readonly userRepository: IUserRepository) {}

  async execute(): Promise<GetAllUsersOutput> {
    const users = await this.userRepository.findAll();

    return {
      users: users.map((user) => user.toObject()),
    };
  }
}

ユーザー更新ユースケース

application/usecases/UpdateUserUseCase.ts
import { IUserRepository } from "../../domain/repositories/IUserRepository";
import { Email } from "../../domain/value-objects/Email";
import { UserId } from "../../domain/value-objects/UserId";
import { UserName } from "../../domain/value-objects/UserName";

export interface UpdateUserInput {
  id: number;
  email?: string;
  name?: string;
}

export interface UpdateUserOutput {
  id: number;
  email: string;
  name: string;
}

export class UpdateUserUseCase {
  constructor(private readonly userRepository: IUserRepository) {}

  async execute(input: UpdateUserInput): Promise<UpdateUserOutput> {
    const userId = new UserId(input.id);

    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new Error("User not found");
    }

    if (input.email) {
      const newEmail = new Email(input.email);
      const exists = await this.userRepository.existsByEmail(newEmail);
      if (exists && !user.getEmail().equals(newEmail)) {
        throw new Error("Email already exists");
      }
      user.changeEmail(newEmail);
    }

    if (input.name) {
      const newName = new UserName(input.name);
      user.changeName(newName);
    }

    const updatedUser = await this.userRepository.update(user);

    return updatedUser.toObject();
  }
}

ユーザー削除ユースケース

application/usecases/DeleteUserUseCase.ts
import { IUserRepository } from "../../domain/repositories/IUserRepository";
import { UserId } from "../../domain/value-objects/UserId";

export interface DeleteUserInput {
  id: number;
}

export class DeleteUserUseCase {
  constructor(private readonly userRepository: IUserRepository) {}

  async execute(input: DeleteUserInput): Promise<void> {
    const userId = new UserId(input.id);

    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new Error("User not found");
    }

    await this.userRepository.delete(userId);
  }
}

まとめ

今回は、DDDにおけるユースケースについて説明しました。

  • ポイント
    • ユースケースはアプリケーションの具体的な操作を表す
    • 1つのユースケースが1つのビジネスフローを実行する
    • executeメソッドで入力と出力を明確にする
    • ドメイン層とインフラ層を協調させる
  • メリット
    • ビジネスフローが明確
    • HTTPから独立しているため再利用が容易
    • テストが書きやすい
    • 変更に強い設計

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?