T-CREATOR

Cline × クリーンアーキテクチャ:ユースケース駆動と境界の切り出し

Cline × クリーンアーキテクチャ:ユースケース駆動と境界の切り出し

AI アシスタントを活用した開発が当たり前になってきた今、コードの品質を保ちながら効率的に開発を進めることが重要です。特に Cline のような AI コーディングツールを使う際、クリーンアーキテクチャの原則に従うことで、保守性と拡張性の高いコードベースを構築できるでしょう。

本記事では、Cline を使った開発においてクリーンアーキテクチャを実践する方法、特にユースケース駆動開発と境界の切り出しに焦点を当てて解説します。AI が生成するコードを構造化し、長期的に保守しやすいアプリケーションを作る実践的な手法をご紹介しますね。

背景

Cline とクリーンアーキテクチャの関係性

Cline は VSCode の拡張機能として動作する AI コーディングアシスタントで、Claude API を活用してコード生成や編集を行います。開発者の指示に基づいて、ファイルの作成、編集、削除を自動的に実行できる強力なツールですね。

しかし、AI が生成するコードは時として構造が曖昧になりがちです。そこで、クリーンアーキテクチャの原則を導入することで、以下のメリットが得られるでしょう。

以下の図は、Cline がクリーンアーキテクチャの各層にどのように関与するかを示しています。

mermaidflowchart TB
    dev["開発者"] -->|指示| cline["Cline AI"]
    cline -->|生成| entities["エンティティ層<br/>ビジネスルール"]
    cline -->|生成| usecases["ユースケース層<br/>アプリケーションロジック"]
    cline -->|生成| adapters["アダプター層<br/>インターフェース"]
    cline -->|生成| frameworks["フレームワーク層<br/>外部ツール・DB"]

    entities -.->|依存| usecases
    usecases -.->|依存| adapters
    adapters -.->|依存| frameworks

    style cline fill:#e1f5ff
    style entities fill:#fff4e6
    style usecases fill:#e8f5e9
    style adapters fill:#f3e5f5
    style frameworks fill:#fce4ec

図で理解できる要点:

  • Cline は各層のコードを生成しますが、依存関係は内側から外側への一方向のみです
  • 開発者は Cline に指示を出し、各層の責務に応じたコード生成を促します
  • エンティティ層が最も内側で、外部の変更に影響されにくい構造になっています

クリーンアーキテクチャでは、ビジネスロジックを中心に据え、外部の技術的詳細から分離します。この分離により、AI が生成したコードであっても、明確な責務と境界を持つモジュールとして整理できますね。

ユースケース駆動開発の重要性

ユースケース駆動開発は、システムが「何をするか」を中心に設計を進める手法です。これは AI アシスタントに指示を出す際、非常に相性が良いアプローチと言えるでしょう。

Cline に対して「ユーザー登録機能を作って」と曖昧に指示するのではなく、「ユーザー登録ユースケースを実装して。メールアドレスの重複チェックと、パスワードのハッシュ化を含めて」と具体的に指示することで、より構造化されたコードを生成できます。

課題

AI 生成コードの一般的な問題点

Cline のような AI ツールを使うと、コード生成は速くなりますが、以下のような課題に直面することが多いですね。

  1. 層の混在: ビジネスロジックとデータアクセスロジックが同じファイルに混在する
  2. 密結合: データベースや外部 API への直接的な依存が多発する
  3. テスト困難: モックやスタブを使ったテストが難しい構造になる
  4. 境界の曖昧さ: どこまでがユースケースで、どこからがインフラ層かが不明瞭になる

以下の図は、構造化されていないコードの問題点を示しています。

mermaidflowchart TD
    controller["Controller"] -->|直接呼び出し| db[("Database")]
    controller -->|直接呼び出し| api["External API"]
    controller -->|ビジネスロジック<br/>を含む| validation["バリデーション"]

    style controller fill:#ffebee
    style db fill:#ffebee
    style api fill:#ffebee
    style validation fill:#ffebee

この図が示す問題点:

  • Controller が複数の責務を持ち、変更の影響範囲が広い
  • データベースや外部 API への直接依存により、テストが困難
  • ビジネスルールの再利用ができない構造になっています

境界が曖昧なコードの影響

境界が曖昧なコードは、以下のような具体的な問題を引き起こします。

#問題具体例影響
1テスト困難データベース接続が必須ユニットテストが書けない
2変更コスト増DB 変更時に複数ファイル修正開発速度の低下
3再利用性低下ロジックが特定の実装に依存コードの重複が発生
4AI の理解困難責務が不明瞭Cline が適切な修正を提案できない

特に 4 番目の問題は見落とされがちですが、重要なポイントです。Cline に「このユーザー登録機能を修正して」と指示しても、コードの境界が曖昧だと、どこをどう修正すべきか AI が判断しづらくなるでしょう。

解決策

ユースケースファーストの設計アプローチ

クリーンアーキテクチャでは、ユースケースを中心に設計を始めます。Cline を使う際も、この原則を守ることで、構造化されたコードを生成できますね。

以下の手順で、ユースケース駆動の開発を進めましょう。

ステップ 1:ユースケースの定義

まず、実現したい機能をユースケースとして言語化します。例えば「ユーザー登録」というユースケースであれば、以下のように定義できます。

typescript// src/domain/usecases/RegisterUserUseCase.ts

/**
 * ユーザー登録ユースケース
 * - メールアドレスの重複をチェック
 * - パスワードをハッシュ化
 * - ユーザーを永続化
 */
export interface RegisterUserUseCase {
  execute(
    input: RegisterUserInput
  ): Promise<RegisterUserOutput>;
}

このインターフェースは、ユースケースの入出力を明確に定義しています。Cline にこのインターフェースを示すことで、実装すべき内容が明確になりますね。

ステップ 2:入出力型の定義

次に、ユースケースの入出力型を定義します。これにより、データの境界が明確になります。

typescript// src/domain/usecases/RegisterUserUseCase.ts

export interface RegisterUserInput {
  email: string;
  password: string;
  username: string;
}

export interface RegisterUserOutput {
  userId: string;
  email: string;
  username: string;
  createdAt: Date;
}

入出力型を分けることで、ユースケースが何を受け取り、何を返すのかが一目瞭然です。Cline にも、この型情報を基に適切なコードを生成するよう指示できるでしょう。

境界の明確な切り出し方

クリーンアーキテクチャの核心は、適切な境界を設けることです。以下の図は、推奨される層の構造を示しています。

mermaidflowchart LR
    subgraph domain["ドメイン層(内側)"]
        entities["エンティティ"]
        usecases["ユースケース<br/>インターフェース"]
    end

    subgraph application["アプリケーション層"]
        impl["ユースケース<br/>実装"]
    end

    subgraph infrastructure["インフラ層(外側)"]
        repo["リポジトリ<br/>実装"]
        db[("Database")]
    end

    impl -->|依存| usecases
    impl -->|使用| repo
    repo -->|アクセス| db

    style domain fill:#e8f5e9
    style application fill:#fff3e0
    style infrastructure fill:#f3e5f5

境界を切り出す際の原則:

  • ドメイン層は外部に依存しない純粋なビジネスロジック
  • アプリケーション層はユースケースの実装を含むが、技術的詳細は含まない
  • インフラ層は具体的な実装(DB、API など)を担当します

依存性逆転の原則の適用

境界を切り出す際、最も重要なのが依存性逆転の原則(DIP)です。これは、具体的な実装ではなく、抽象(インターフェース)に依存させる原則ですね。

まず、リポジトリのインターフェースをドメイン層で定義します。

typescript// src/domain/repositories/UserRepository.ts

import { User } from '../entities/User';

/**
 * ユーザーリポジトリインターフェース
 * ドメイン層で定義し、インフラ層で実装する
 */
export interface UserRepository {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
  findById(id: string): Promise<User | null>;
}

このインターフェースをユースケースで使用することで、具体的なデータベース実装から分離できます。

次に、ユースケースの実装で、このインターフェースに依存します。

typescript// src/application/usecases/RegisterUserUseCaseImpl.ts

import {
  RegisterUserUseCase,
  RegisterUserInput,
  RegisterUserOutput,
} from '../../domain/usecases/RegisterUserUseCase';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User } from '../../domain/entities/User';

export class RegisterUserUseCaseImpl
  implements RegisterUserUseCase
{
  constructor(private userRepository: UserRepository) {}

  // ユースケースの実装は次のセクションで説明
}

コンストラクタで UserRepository インターフェースを受け取ることで、具体的な実装への依存を排除しています。Cline にこのパターンを指示する際も、「依存性注入を使って」と明示すると良いですね。

Cline への効果的な指示の出し方

Cline にクリーンアーキテクチャを守らせるには、指示の出し方が重要です。以下のような指示が効果的でしょう。

悪い指示例:

ユーザー登録機能を作って

良い指示例:

diffRegisterUserUseCase インターフェースを実装する RegisterUserUseCaseImpl クラスを作成してください。
- UserRepository インターフェースに依存させること
- パスワードハッシュ化には bcrypt を使用
- メールアドレスの重複チェックを実装
- src/application/usecases/ に配置

このように具体的に指示することで、Cline は適切な場所に適切なコードを生成できます。

具体例

エンティティの定義

クリーンアーキテクチャでは、まずエンティティ(ビジネスルール)を定義します。これは最も内側の層で、外部の変更に影響されません。

typescript// src/domain/entities/User.ts

/**
 * ユーザーエンティティ
 * ビジネスルールを含む純粋なドメインオブジェクト
 */
export class User {
  constructor(
    public readonly id: string,
    public readonly email: string,
    public readonly passwordHash: string,
    public readonly username: string,
    public readonly createdAt: Date
  ) {}

エンティティにビジネスルールを含める場合は、メソッドとして実装します。

typescript  /**
   * メールアドレスのバリデーション
   * ビジネスルールとしてエンティティに含める
   */
  static isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

エンティティの残りの部分を実装します。

typescript  /**
   * パスワードの強度チェック
   * 最低8文字、英数字を含む
   */
  static isStrongPassword(password: string): boolean {
    if (password.length < 8) return false;
    const hasLetter = /[a-zA-Z]/.test(password);
    const hasNumber = /[0-9]/.test(password);
    return hasLetter && hasNumber;
  }
}

これらのバリデーションルールは、データベースや UI フレームワークに依存しない純粋なビジネスロジックです。Cline にエンティティを作成させる際は、「ビジネスルールのみを含めて」と指示すると良いでしょう。

ユースケースの実装

次に、ユースケースを実装します。ここでは、エンティティとリポジトリインターフェースを使用しますが、具体的な実装には依存しません。

typescript// src/application/usecases/RegisterUserUseCaseImpl.ts

import {
  RegisterUserUseCase,
  RegisterUserInput,
  RegisterUserOutput,
} from '../../domain/usecases/RegisterUserUseCase';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User } from '../../domain/entities/User';
import * as bcrypt from 'bcrypt';

必要な依存関係をインポートします。ここで注目すべきは、すべてインターフェースに依存している点ですね。

次に、ユースケースクラスの基本構造を定義します。

typescriptexport class RegisterUserUseCaseImpl implements RegisterUserUseCase {
  private readonly SALT_ROUNDS = 10;

  constructor(private userRepository: UserRepository) {}

コンストラクタで UserRepository を受け取り、プライベートフィールドに格納しています。これが依存性注入の基本パターンです。

ユースケースのメイン処理を実装します。

typescript  async execute(input: RegisterUserInput): Promise<RegisterUserOutput> {
    // 1. 入力バリデーション
    if (!User.isValidEmail(input.email)) {
      throw new Error('Invalid email format');
    }

    if (!User.isStrongPassword(input.password)) {
      throw new Error('Password must be at least 8 characters with letters and numbers');
    }

エンティティに定義したバリデーションルールを使用して、入力をチェックします。

メールアドレスの重複チェックとユーザー作成処理を実装します。

typescript// 2. メールアドレスの重複チェック
const existingUser = await this.userRepository.findByEmail(
  input.email
);
if (existingUser) {
  throw new Error('Email already registered');
}

// 3. パスワードのハッシュ化
const passwordHash = await bcrypt.hash(
  input.password,
  this.SALT_ROUNDS
);

リポジトリインターフェースを通じてデータアクセスを行い、具体的な実装からは分離されています。

最後に、ユーザーを保存して結果を返します。

typescript    // 4. ユーザーエンティティの作成と保存
    const user = new User(
      this.generateId(),
      input.email,
      passwordHash,
      input.username,
      new Date()
    );

    const savedUser = await this.userRepository.save(user);

    // 5. 出力への変換
    return {
      userId: savedUser.id,
      email: savedUser.email,
      username: savedUser.username,
      createdAt: savedUser.createdAt
    };
  }

  private generateId(): string {
    return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

この実装により、ビジネスロジックが明確に分離され、テスト可能な構造になりました。

リポジトリの実装(インフラ層)

最後に、インフラ層でリポジトリの具体的な実装を行います。ここでは例として TypeORM を使用しますが、他の ORM でも同様です。

typescript// src/infrastructure/repositories/TypeORMUserRepository.ts

import { Repository } from 'typeorm';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User } from '../../domain/entities/User';
import { UserEntity } from '../entities/UserEntity';

TypeORM の Repository と、ドメイン層のインターフェースをインポートします。

リポジトリクラスの基本構造を定義します。

typescript/**
 * TypeORM を使用した UserRepository の実装
 * インフラ層に配置し、技術的詳細を担当
 */
export class TypeORMUserRepository implements UserRepository {
  constructor(private ormRepository: Repository<UserEntity>) {}

TypeORM の Repository を受け取り、内部で使用します。

findByEmail メソッドを実装します。

typescript  async findByEmail(email: string): Promise<User | null> {
    const userEntity = await this.ormRepository.findOne({
      where: { email }
    });

    if (!userEntity) return null;

    return this.toDomain(userEntity);
  }

データベースから取得したエンティティを、ドメインのエンティティに変換しています。

save メソッドと変換メソッドを実装します。

typescript  async save(user: User): Promise<User> {
    const userEntity = this.toEntity(user);
    const saved = await this.ormRepository.save(userEntity);
    return this.toDomain(saved);
  }

  async findById(id: string): Promise<User | null> {
    const userEntity = await this.ormRepository.findOne({
      where: { id }
    });

    if (!userEntity) return null;

    return this.toDomain(userEntity);
  }

各メソッドで、データベース層とドメイン層の境界を明確に保っています。

エンティティ変換メソッドを実装します。

typescript  /**
   * DB エンティティからドメインエンティティへ変換
   */
  private toDomain(entity: UserEntity): User {
    return new User(
      entity.id,
      entity.email,
      entity.passwordHash,
      entity.username,
      entity.createdAt
    );
  }

  /**
   * ドメインエンティティから DB エンティティへ変換
   */
  private toEntity(user: User): UserEntity {
    const entity = new UserEntity();
    entity.id = user.id;
    entity.email = user.email;
    entity.passwordHash = user.passwordHash;
    entity.username = user.username;
    entity.createdAt = user.createdAt;
    return entity;
  }
}

この変換により、ドメイン層は TypeORM の存在を知らず、完全に分離されています。

依存性の注入と組み立て

すべての部品が揃ったら、依存性注入を使って組み立てます。以下は、Express を使った例ですね。

typescript// src/infrastructure/http/routes/userRoutes.ts

import { Router } from 'express';
import { getRepository } from 'typeorm';
import { UserEntity } from '../../entities/UserEntity';
import { TypeORMUserRepository } from '../../repositories/TypeORMUserRepository';
import { RegisterUserUseCaseImpl } from '../../../application/usecases/RegisterUserUseCaseImpl';

必要な依存関係をすべてインポートします。

ルーターの設定と依存性の組み立てを行います。

typescriptconst router = Router();

/**
 * ユーザー登録エンドポイント
 * POST /api/users/register
 */
router.post('/register', async (req, res) => {
  try {
    // 1. リポジトリの作成
    const ormRepository = getRepository(UserEntity);
    const userRepository = new TypeORMUserRepository(ormRepository);

TypeORM のリポジトリを取得し、それをラップした実装を作成します。

ユースケースを実行して、結果を返します。

typescript// 2. ユースケースの作成と実行
const useCase = new RegisterUserUseCaseImpl(userRepository);
const result = await useCase.execute({
  email: req.body.email,
  password: req.body.password,
  username: req.body.username,
});

// 3. レスポンスの返却
res.status(201).json({
  success: true,
  data: result,
});

ユースケースに依存性を注入し、実行します。

エラーハンドリングを実装します。

typescript  } catch (error) {
    console.error('Registration error:', error);
    res.status(400).json({
      success: false,
      message: error.message || 'Registration failed'
    });
  }
});

export default router;

この構造により、各層が明確に分離され、テストや変更が容易になります。

完成したアーキテクチャの全体像

以下の図は、実装した各コンポーネントの関係性を示しています。

mermaidflowchart TB
    subgraph presentation["プレゼンテーション層"]
        route["Express Route"]
    end

    subgraph application["アプリケーション層"]
        usecase["RegisterUserUseCaseImpl"]
    end

    subgraph domain["ドメイン層"]
        interface["RegisterUserUseCase<br/>Interface"]
        repoInterface["UserRepository<br/>Interface"]
        entity["User Entity"]
    end

    subgraph infrastructure["インフラ層"]
        repo["TypeORMUserRepository"]
        db[("PostgreSQL")]
    end

    route -->|使用| usecase
    usecase -->|実装| interface
    usecase -->|依存| repoInterface
    usecase -->|使用| entity
    repo -->|実装| repoInterface
    repo -->|アクセス| db

    style domain fill:#e8f5e9
    style application fill:#fff3e0
    style infrastructure fill:#f3e5f5
    style presentation fill:#e3f2fd

この図から読み取れるポイント:

  • 依存関係が常に内側(ドメイン層)に向かっている
  • ドメイン層は他の層に依存せず、最も安定している
  • インフラ層の変更(例:TypeORM から Prisma への移行)は、ドメイン層に影響しません
  • プレゼンテーション層(Express Route)の変更も、ビジネスロジックに影響しない構造です

Cline でのテスト駆動開発

クリーンアーキテクチャの利点は、テストが容易になることです。Cline にテストコードを生成させる際も、境界が明確なので効率的ですね。

typescript// src/application/usecases/__tests__/RegisterUserUseCaseImpl.test.ts

import { RegisterUserUseCaseImpl } from '../RegisterUserUseCaseImpl';
import { UserRepository } from '../../../domain/repositories/UserRepository';
import { User } from '../../../domain/entities/User';

/**
 * ユースケースのユニットテスト
 * モックリポジトリを使用して、インフラ層から独立してテスト
 */
describe('RegisterUserUseCaseImpl', () => {
  let useCase: RegisterUserUseCaseImpl;
  let mockRepository: jest.Mocked<UserRepository>;

モックを使用することで、データベースなしでテストできます。

テストケースのセットアップを行います。

typescriptbeforeEach(() => {
  // モックリポジトリの作成
  mockRepository = {
    findByEmail: jest.fn(),
    save: jest.fn(),
    findById: jest.fn(),
  };

  useCase = new RegisterUserUseCaseImpl(mockRepository);
});

各テストの前に、新しいモックとユースケースインスタンスを作成します。

成功ケースのテストを実装します。

typescript  it('should register a new user successfully', async () => {
    // Arrange
    mockRepository.findByEmail.mockResolvedValue(null);
    mockRepository.save.mockResolvedValue(
      new User('user_123', 'test@example.com', 'hashed', 'testuser', new Date())
    );

    // Act
    const result = await useCase.execute({
      email: 'test@example.com',
      password: 'Password123',
      username: 'testuser'
    });

AAA パターン(Arrange-Act-Assert)でテストを構造化しています。

テストのアサーション部分を実装します。

typescript    // Assert
    expect(result.userId).toBe('user_123');
    expect(result.email).toBe('test@example.com');
    expect(mockRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
    expect(mockRepository.save).toHaveBeenCalled();
  });

期待される結果と、モックの呼び出しを検証しています。

エラーケースのテストを実装します。

typescriptit('should throw error when email already exists', async () => {
  // Arrange
  mockRepository.findByEmail.mockResolvedValue(
    new User(
      'existing',
      'test@example.com',
      'hashed',
      'existing',
      new Date()
    )
  );

  // Act & Assert
  await expect(
    useCase.execute({
      email: 'test@example.com',
      password: 'Password123',
      username: 'testuser',
    })
  ).rejects.toThrow('Email already registered');
});

境界が明確なため、様々なシナリオを簡単にテストできますね。

バリデーションエラーのテストを追加します。

typescript  it('should throw error for invalid email format', async () => {
    await expect(useCase.execute({
      email: 'invalid-email',
      password: 'Password123',
      username: 'testuser'
    })).rejects.toThrow('Invalid email format');
  });

  it('should throw error for weak password', async () => {
    await expect(useCase.execute({
      email: 'test@example.com',
      password: 'weak',
      username: 'testuser'
    })).rejects.toThrow('Password must be at least 8 characters');
  });
});

Cline に「このユースケースのテストを書いて」と指示すれば、このような包括的なテストコードを生成できます。

まとめ

Cline とクリーンアーキテクチャを組み合わせることで、AI の生産性とコードの品質を両立できます。本記事で紹介したユースケース駆動と境界の切り出しは、長期的に保守可能なシステムを構築する基盤となるでしょう。

重要なポイントの振り返り:

#ポイント効果Cline への指示
1ユースケースファーストビジネスロジックの明確化「〇〇ユースケースを実装して」
2依存性逆転の原則テスト容易性の向上「インターフェースに依存させて」
3層の明確な分離変更影響の局所化「ドメイン層に配置して」
4具体的な指示AI の理解精度向上詳細な要件と配置場所を指定

AI アシスタントは強力なツールですが、構造化されたアーキテクチャがあってこそ、その真価を発揮します。クリーンアーキテクチャの原則に従うことで、Cline が生成するコードを整理し、長期的に価値のある資産として育てていけるでしょう。

まずは小さなユースケースから始めて、徐々に境界を明確にしていくアプローチをおすすめします。Cline に適切な指示を出し、生成されたコードをレビューしながら、理想的なアーキテクチャへと近づけていってくださいね。

クリーンアーキテクチャは最初は複雑に感じるかもしれませんが、一度構造が確立されれば、新機能の追加やリファクタリングが驚くほどスムーズになります。AI と協力しながら、保守性の高いコードベースを構築していきましょう。

関連リンク