3
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptで学ぶ依存性逆転の原則 〜テスト可能なコードへのリファクタリング実践〜

目次


1. はじめに

この記事で学べること

この記事では、シンプルな図書館アプリケーションを題材に、以下の内容を学びます。

  • クラス間の強い依存関係がもたらす問題点
  • interfaceを使った依存関係の解消方法
  • **依存性注入(Dependency Injection)**パターンの実践
  • Jestを使ったユニットテストの書き方
  • モックを活用したテスト手法

実際のコードを段階的にリファクタリングしながら、「なぜこの設計が必要なのか」を体感していただきます。

対象読者

  • TypeScriptの基本文法を理解している方
  • クラスやinterfaceの構文は知っているが、「なぜinterfaceを使うのか」がピンと来ていない方
  • テストを書きたいが、どう設計すればテストしやすくなるか分からない方
  • レイヤードアーキテクチャやクリーンアーキテクチャに興味がある方

2. プロジェクトのセットアップ

まず、今回使用するプロジェクトをセットアップします。

ディレクトリ構成

最終的なディレクトリ構成は以下の通りです。

library-app/
├── prisma/
│   ├── generated/prisma/    # Prismaが生成するクライアント
│   └── schema.prisma        # データベーススキーマ定義
├── src/
│   ├── buisinessLogic/      # ビジネスロジック層
│   │   ├── bookService.ts
│   │   └── bookServiceInterface.ts
│   ├── dataAccess/          # データアクセス層
│   │   ├── prismaBookRepository.ts
│   │   └── bookRepositoryInterface.ts
│   ├── presentation/        # プレゼンテーション層
│   │   └── bookController.ts
│   └── app.ts               # エントリーポイント
├── package.json
├── tsconfig.json
├── jest.config.js
└── prisma.config.ts

package.json

以下のpackage.jsonをプロジェクトルートに作成してください。

{
  "name": "library-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "tsx watch src/app.ts",
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@prisma/adapter-better-sqlite3": "^7.3.0",
    "@prisma/client": "^7.3.0",
    "express": "^5.2.1"
  },
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  },
  "devDependencies": {
    "@types/express": "^5.0.6",
    "@types/jest": "^30.0.0",
    "@types/node": "^25.1.0",
    "jest": "^30.2.0",
    "prisma": "^7.3.0",
    "ts-jest": "^29.4.6",
    "ts-node-dev": "^2.0.0",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3"
  }
}

注意: "type": "module" を指定しているため、プロジェクト全体がESM(ECMAScript Modules)として動作します。

tsconfig.json

{
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "bundler",
    "types": ["jest", "node"],

    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true
  }
}

Prismaの設定

prisma/schema.prisma

generator client {
  provider = "prisma-client"
  output   = "./generated/prisma"
}

datasource db {
  provider = "sqlite"
}

model Book {
  id String @id @default(uuid())
  title String
  isAvailable Boolean
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

prisma.config.ts

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "npx tsx prisma/seed.ts",
  },
  datasource: {
    url: process.env["DATABASE_URL"] ?? "file:./prisma/dev.db",
  },
});

依存関係のインストール

以下のコマンドを実行して、依存関係をインストールします。

# プロジェクトディレクトリを作成
mkdir library-app
cd library-app

# package.jsonを配置後、依存関係をインストール
npm install

# Prismaクライアントを生成
npx prisma generate

# データベースのマイグレーション
npx prisma migrate dev --name init

3. 問題のあるコード(Before)

アプリケーション全体像

今回のアプリケーションは、シンプルな図書館の書籍管理システムです。以下の3層構造になっています。

┌─────────────────────────────────────────────────────────────┐
│                    Presentation層                           │
│                  (BookController)                           │
│              HTTPリクエスト/レスポンスの処理                  │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                   Business Logic層                          │
│                    (BookService)                            │
│                 ビジネスロジックの実行                        │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Data Access層                            │
│                (PrismaBookRepository)                       │
│              データベースとのやり取り                         │
└─────────────────────────────────────────────────────────────┘

各ファイルの役割と実装

まず、**問題のあるコード(interface導入前)**を見ていきましょう。

src/dataAccess/prismaBookClient.ts(Data Access層)

データベースとの実際のやり取りを担当します。

import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient, type Book } from "../../prisma/generated/prisma/client";

const adapter = new PrismaBetterSqlite3({
  url: process.env.DATABASE_URL || "file:./dev.db",
});

export class PrismaBookRepository {
  private prisma: PrismaClient;

  constructor(){
    this.prisma = new PrismaClient({ adapter });
  }

  createBook = async (title: string): Promise<Book> => {
    return await this.prisma.book.create({
      data:{
        title,
        isAvailable: true,
      }
    })
  }

  findBookByID = async (id: string): Promise<Book | null> => {
    return await this.prisma.book.findUnique({
      where: { id },
    });
  };

  findAllBooks = async (): Promise<Book[]> => {
    return await this.prisma.book.findMany();
  }
}

役割: Prismaを使ってSQLiteデータベースに対するCRUD操作を行います。


src/buisinessLogic/bookService.ts(Business Logic層)

ビジネスロジックを担当します。

import { PrismaBookRepository } from "../dataAccess/prismaBookClient";
import type { Book } from "../../prisma/generated/prisma/client";

export class BookService {
  private bookRepository: PrismaBookRepository;

  constructor(){
    this.bookRepository = new PrismaBookRepository;  // 直接インスタンス化
  }

  addBook = async (title: string): Promise<Book> => {
    return await this.bookRepository.createBook(title);
  }

  findBookByID = async (id: string): Promise<Book | null> => {
    return await this.bookRepository.findBookByID(id);
  }

  findAllBooks = async (): Promise<Book[]> => {
    return await this.bookRepository.findAllBooks();
  }
}

問題点: constructor内でPrismaBookRepositoryを直接newしています。


src/presentation/bookController.ts(Presentation層)

HTTPリクエストを受け取り、レスポンスを返します。

import type { Request, Response } from "express";
import { BookService } from "../buisinessLogic/bookService";

export class BookController {
  private bookSerivce: BookService;

  constructor(){
    this.bookSerivce = new BookService();  // 直接インスタンス化
  }

  addBook = async (req: Request, res: Response): Promise<void> => {
    try {
      const title = req.body.title as string;
      const book = await this.bookSerivce.addBook(title);
      res.status(201).json(book);
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の登録に失敗しました" });
    }
  }

  findBookByID = async (req: Request, res: Response): Promise<void> => {
    try {
      const id = req.params.id as string;
      const book = await this.bookSerivce.findBookByID(id);
      if (book) {
        res.status(200).json(book);
      } else {
        res.status(404).json({ error: "書籍が見つかりませんでした" });
      }
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の検索に失敗しました" });
    }
  }

  findAllBooks = async (req: Request, res: Response): Promise<void> => {
    try {
      const books = await this.bookSerivce.findAllBooks();
      res.status(200).json(books);
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の検索に失敗しました" });
    }
  }
}

問題点: ここでもBookServiceを直接newしています。


src/app.ts(エントリーポイント)

import express from 'express';
import { BookController } from './presentation/bookController';

const app = express();
app.use(express.json());

const PORT = process.env.PORT || 3009;

app.get('/', (req, res) => {
  res.send('Hello World');
});

const bookController = new BookController();

app.post('/books', (req, res) => bookController.addBook(req, res));
app.get('/books/:id', (req, res) => bookController.findBookByID(req, res));
app.get('/books', (req, res) => bookController.findAllBooks(req, res));

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

クラス間の強い依存関係とは

上記のコードには**強い依存関係(密結合)**があります。図で表すと以下のようになります。

┌──────────────────┐
│  BookController  │
│                  │
│  constructor(){  │
│    this.service  │──────────┐
│    = new         │          │ 具体クラスを直接new
│    BookService() │          │
│  }               │          ▼
└──────────────────┘   ┌──────────────────┐
                       │   BookService    │
                       │                  │
                       │  constructor(){  │
                       │    this.repo     │──────────┐
                       │    = new         │          │ 具体クラスを直接new
                       │    PrismaBook    │          │
                       │    Repository()  │          ▼
                       │  }               │   ┌──────────────────────┐
                       └──────────────────┘   │ PrismaBookRepository │
                                              │                      │
                                              │   実際のDB接続       │
                                              │   SQLiteを操作       │
                                              └──────────────────────┘

「強い依存関係」とは:

  • 上位のクラス(BookService)が下位のクラス(PrismaBookRepository)の具体的な実装クラスを直接知っている
  • newキーワードで直接インスタンスを生成している
  • 下位クラスを変更すると、上位クラスにも影響が及ぶ

なぜこのコードはテストしにくいのか

最大の問題: BookServiceをテストしようとすると、必ず実際のデータベースが必要になる

// BookServiceをテストしたい
const service = new BookService();

// しかし、constructor内で PrismaBookRepository を new している
// → PrismaBookRepository は実際のDBに接続しようとする
// → テスト用のDBを用意しないとテストできない!

この設計がもたらす問題:

問題 説明
テストが遅い 毎回実際のDBにアクセスするため、テスト実行に時間がかかる
テストが不安定 DB接続の問題やデータ状態に依存してテストが失敗する可能性がある
テストの独立性がない テスト間でDBのデータが共有され、テストの順序に依存してしまう
差し替えができない 本番用のDBを使わざるを得ず、モックに差し替えられない
【テストしたい範囲】
┌─────────────────────────────────────────┐
│            BookService                  │ ← ここだけテストしたい
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│        PrismaBookRepository             │ ← でもこれも動いてしまう
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│          実際のデータベース               │ ← これも動いてしまう!
└─────────────────────────────────────────┘

4. interfaceを導入して改善(After)

依存性逆転の原則とは

**依存性逆転の原則(Dependency Inversion Principle: DIP)**は、SOLID原則のひとつです。

「上位モジュールは下位モジュールに依存してはならない。両者は抽象(interface)に依存すべきである」

【Before: 具体に依存】
BookService ───────────→ PrismaBookRepository(具体クラス)

【After: 抽象に依存】
BookService ───────────→ BookRepositoryInterface(抽象)
                                    △
                                    │ implements
                         PrismaBookRepository(具体クラス)

ポイント: 矢印の向き(依存の方向)が逆転しています。


interfaceを使ったリファクタリング手順

Step 1: interfaceを定義する

まず、Repository層のinterfaceを作成します。

src/dataAccess/bookRepositoryInterface.ts

import type { Book } from "../../prisma/generated/prisma/client";

export interface BookRepositoryInterface {
  createBook: (title: string) => Promise<Book>;
  findBookByID: (id: string) => Promise<Book | null>;
  findAllBooks: () => Promise<Book[]>;
}

同様に、Service層のinterfaceも作成します。

src/buisinessLogic/bookServiceInterface.ts

import type { Book } from "../../prisma/generated/prisma/client";

export interface BookServiceInterface {
  addBook: (title: string) => Promise<Book>;
  findBookByID: (id: string) => Promise<Book | null>;
  findAllBooks: () => Promise<Book[]>;
}

Step 2: 具体クラスにinterfaceを実装させる

src/dataAccess/prismaBookRepository.ts

import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient, type Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "./bookRepositoryInterface";

const adapter = new PrismaBetterSqlite3({
  url: process.env.DATABASE_URL || "file:./dev.db",
});

// interfaceを実装(implements)
export class PrismaBookRepository implements BookRepositoryInterface {
  private prisma: PrismaClient;

  constructor(){
    this.prisma = new PrismaClient({ adapter });
  }

  createBook = async (title: string): Promise<Book> => {
    return await this.prisma.book.create({
      data:{
        title,
        isAvailable: true,
      }
    })
  }

  findBookByID = async (id: string): Promise<Book | null> => {
    return await this.prisma.book.findUnique({
      where: { id },
    });
  };

  findAllBooks = async (): Promise<Book[]> => {
    return await this.prisma.book.findMany();
  }
}

依存性注入(DI)パターンの導入

次に、**依存性注入(Dependency Injection)**を導入します。

依存性注入とは: クラスが必要とする依存オブジェクトを、外部から渡す(注入する)設計パターン

Before(自分でnewする):

class BookService {
  private bookRepository: PrismaBookRepository;

  constructor(){
    this.bookRepository = new PrismaBookRepository();  // 自分で作る
  }
}

After(外部から受け取る):

class BookService {
  constructor(private bookRepository: BookRepositoryInterface){  // 外部から受け取る
  }
}

改善後のコード全体像

src/buisinessLogic/bookService.ts

import type { Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "../dataAccess/bookRepositoryInterface";
import type { BookServiceInterface } from "./bookServiceInterface";

export class BookService implements BookServiceInterface {
  // コンストラクタで外部からRepositoryを受け取る(依存性注入)
  constructor(private bookRepository: BookRepositoryInterface){
  }

  addBook = async (title: string): Promise<Book> => {
    return await this.bookRepository.createBook(title);
  }

  findBookByID = async (id: string): Promise<Book | null> => {
    return await this.bookRepository.findBookByID(id);
  }

  findAllBooks = async (): Promise<Book[]> => {
    return await this.bookRepository.findAllBooks();
  }
}

改善点: BookRepositoryInterfaceという抽象に依存し、具体的な実装は外部から注入されます。


src/presentation/bookController.ts

import type { Request, Response } from "express";
import type { BookServiceInterface } from "../buisinessLogic/bookServiceInterface";

export class BookController {
  // コンストラクタで外部からServiceを受け取る(依存性注入)
  constructor(private readonly bookService: BookServiceInterface){
  }

  addBook = async (req: Request, res: Response): Promise<void> => {
    try {
      const title = req.body.title as string;
      const book = await this.bookService.addBook(title);
      res.status(201).json(book);
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の登録に失敗しました" });
    }
  }

  findBookByID = async (req: Request, res: Response): Promise<void> => {
    try {
      const id = req.params.id as string;
      const book = await this.bookService.findBookByID(id);
      if (book) {
        res.status(200).json(book);
      } else {
        res.status(404).json({ error: "書籍が見つかりませんでした" });
      }
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の検索に失敗しました" });
    }
  }

  findAllBooks = async (req: Request, res: Response): Promise<void> => {
    try {
      const books = await this.bookService.findAllBooks();
      res.status(200).json(books);
    } catch (error) {
      console.log(error);
      res.status(500).json({ error: "書籍の検索に失敗しました" });
    }
  }
}

src/app.ts(組み立てを行う場所)

import express from 'express';
import { BookController } from './presentation/bookController';
import { BookService } from './buisinessLogic/bookService';
import { PrismaBookRepository } from './dataAccess/prismaBookRepository';

const app = express();
app.use(express.json());

const PORT = process.env.PORT || 3009;

app.get('/', (req, res) => {
  res.send('Hello World');
});

// ここで依存関係を組み立てる(Composition Root)
const bookRepository = new PrismaBookRepository();  // 具体的な実装を生成
const bookService = new BookService(bookRepository); // Serviceに注入
const bookController = new BookController(bookService); // Controllerに注入

app.post('/books', (req, res) => bookController.addBook(req, res));
app.get('/books/:id', (req, res) => bookController.findBookByID(req, res));
app.get('/books', (req, res) => bookController.findAllBooks(req, res));

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

ポイント: 依存関係の組み立てはapp.ts(Composition Root)で一元管理されます。


改善後の依存関係図:

┌──────────────────┐        ┌──────────────────────────┐
│  BookController  │───────→│  BookServiceInterface    │ ← 抽象に依存
└──────────────────┘        └──────────────────────────┘
                                        △
                                        │ implements
                            ┌──────────────────────────┐
                            │      BookService         │
                            └──────────────────────────┘
                                        │
                                        ▼
                            ┌──────────────────────────┐
                            │ BookRepositoryInterface  │ ← 抽象に依存
                            └──────────────────────────┘
                                        △
                                        │ implements
                            ┌──────────────────────────┐
                            │  PrismaBookRepository    │
                            └──────────────────────────┘

5. テストを書いてメリットを実感

Jest + ts-jestのセットアップ

ESMプロジェクトでJestを使用するための設定を行います。

jest.config.js

/** @type {import('jest').Config} */
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', {
      useESM: true,
    }],
  },
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
};

注意: package.json"type": "module"が設定されているため、.cjsではなくESM形式の.jsで設定ファイルを作成しています。export defaultを使用しているのがポイントです。

tsconfig.jsonの確認

tsconfig.jsontypes配列にjestが含まれていることを確認してください。

{
  "compilerOptions": {
    "types": ["jest", "node"],
    // ...
  }
}

これにより、describeitexpectなどのJestのグローバル関数がTypeScriptで認識されます。


モックとは何か

**モック(Mock)**とは、テスト対象が依存しているオブジェクトの「偽物」です。

【本番環境】
BookService ────→ PrismaBookRepository ────→ 実際のDB

【テスト環境】
BookService ────→ モック(偽のRepository) ────→ DBアクセスなし

モックの役割:

機能 説明
呼び出しの記録 どのメソッドが、何回、どんな引数で呼ばれたかを記録
戻り値の制御 任意の値を返すように設定できる
外部依存の排除 実際のDB、API、ファイルシステムへのアクセスを回避
// jest.fn() でモック関数を作成
const mockFunction = jest.fn();

// 呼び出すと記録される
mockFunction("hello");

// 呼び出されたことを検証できる
expect(mockFunction).toHaveBeenCalledWith("hello");

// 戻り値を設定することもできる
mockFunction.mockReturnValue("world");

実際にモックを使ったテストを書く

src/buisinessLogic/bookService.test.ts

import type { Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "../dataAccess/bookRepositoryInterface";
import { BookService } from "./bookService";

// モックオブジェクトを作成
// BookRepositoryInterfaceの各メソッドにjest.fn()を割り当てる
const mockBookRepository: jest.Mocked<BookRepositoryInterface> = {
  createBook: jest.fn(),   // 仮の関数を作成
  findBookByID: jest.fn(),
  findAllBooks: jest.fn(),
};

describe('BookService', () => {
  let bookService: BookService;

  beforeEach(() => {
    // 各テストの前に、モックを注入したBookServiceを作成
    bookService = new BookService(mockBookRepository);
  });

  afterEach(() => {
    // 各テストの後に、モックの呼び出し履歴をクリア
    jest.clearAllMocks();
  });

  it('書籍の登録が成功する', async () => {
    // テスト用のデータを準備
    const newBook: Book = {
      id: '1',
      title: 'test book',
      isAvailable: true,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    // モックの戻り値を設定
    // createBookが呼ばれたら、newBookを返すように設定
    mockBookRepository.createBook.mockResolvedValue(newBook);

    // テスト対象のメソッドを実行
    const result = await bookService.addBook('test book');

    // 検証1: 戻り値が正しいこと
    expect(result).toEqual(newBook);

    // 検証2: Repositoryのメソッドが正しい引数で呼ばれたこと
    expect(mockBookRepository.createBook).toHaveBeenCalledWith('test book');
  });
});

テストを実行:

npm test

実行結果:

PASS src/buisinessLogic/bookService.test.ts
  BookService
    ✓ 書籍の登録が成功する (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

なぜこの方法でモックするのか

答え: BookServiceがinterfaceに依存しているため、クリーンにモックを注入できる

// BookServiceのコンストラクタ
constructor(private bookRepository: BookRepositoryInterface){
}

BookRepositoryInterfaceという抽象に依存しているため、その実装は何でも良いのです。

【本番】BookRepositoryInterfaceを満たす → PrismaBookRepository を渡す
【テスト】BookRepositoryInterfaceを満たす → モックオブジェクトを渡す
┌─────────────────────────────────────────┐
│     BookRepositoryInterface             │ ← 契約(約束事)
│  ・createBook()を持つこと                │
│  ・findBookByID()を持つこと              │
│  ・findAllBooks()を持つこと              │
└─────────────────────────────────────────┘
          △                    △
          │                    │
          │implements          │満たしている
          │                    │
┌─────────────────┐    ┌─────────────────┐
│ PrismaBook      │    │ モック           │
│ Repository      │    │ オブジェクト      │
│                 │    │                 │
│ 本物のDB接続    │    │ 呼び出し記録のみ  │
└─────────────────┘    └─────────────────┘

補足: interfaceがなくてもモック自体は可能

注意: 実はjest.mock()を使えば、interfaceがなくてもモックは可能です。

// jest.mock()を使った方法(interfaceなし)
import { BookService } from "./bookService";
import { PrismaBookRepository } from "../dataAccess/prismaBookClient";

jest.mock("../dataAccess/prismaBookClient");  // モジュール全体をモック化

const service = new BookService();
// → PrismaBookRepositoryが自動的にモックに置き換わる

では、なぜわざわざinterfaceを導入するのか?

観点 jest.mock()のみ interface + DI
可読性 何がモックか分かりにくい 明示的で分かりやすい
リファクタリング耐性 ファイルパスに依存や具体の実装に依存 interfaceのみに依存(安定)
拡張性 実装の差し替えが困難 新しい実装を簡単に追加可能

結論: モック「できるかどうか」ではなく、「クリーンにできるかどうか」と「設計が改善されるかどうか」が重要です。

interface + DIパターンを使うことで、テストのためだけでなく、本番コードの設計自体が良くなります。例えば、将来「PostgreSQLに変更したい」「インメモリDBでテストしたい」となった場合も、新しい実装クラスを作って差し替えるだけで対応できます。


6. まとめと今後の展望

今回の改善で得られたもの

Before After
具体クラスに直接依存 interfaceに依存
クラス内でnewする 外部から注入される(DI)
モックは可能だが設定が複雑 クリーンにモックを注入できる
テストにDBが必要(または複雑なjest.mock()が必要) DBなしで高速テスト
変更に弱い(密結合) 変更に強い(疎結合)

今回学んだ3つの重要な概念:

  1. 依存性逆転の原則: 具体ではなく抽象に依存する
  2. 依存性注入(DI): 必要なものは外部から受け取る
  3. モック: interfaceを満たす偽オブジェクトでテストを効率化

クリーンアーキテクチャへの道(次回予告)

今回実装したレイヤードアーキテクチャは、クリーンアーキテクチャへの第一歩です。

次回の記事では、以下の内容を扱う予定です。

  • ドメイン層の導入: ビジネスルールをより明確に分離
  • ユースケース層: アプリケーション固有のビジネスロジックの整理
  • 依存関係のルール: 「内側から外側への依存禁止」の徹底
  • より柔軟な設計: フレームワークやDBの差し替えが容易なアーキテクチャ
       ┌─────────────────────────────────────────┐
       │            Frameworks & Drivers         │
       │    (Express, Prisma, etc.)              │
       │  ┌───────────────────────────────────┐  │
       │  │       Interface Adapters          │  │
       │  │   (Controllers, Repositories)     │  │
       │  │  ┌─────────────────────────────┐  │  │
       │  │  │      Application Layer      │  │  │
       │  │  │       (Use Cases)           │  │  │
       │  │  │  ┌───────────────────────┐  │  │  │
       │  │  │  │    Domain Layer       │  │  │  │
       │  │  │  │   (Entities, Rules)   │  │  │  │
       │  │  │  └───────────────────────┘  │  │  │
       │  │  └─────────────────────────────┘  │  │
       │  └───────────────────────────────────┘  │
       └─────────────────────────────────────────┘

乞うご期待!


参考リポジトリ: https://github.com/arunbababa/learn-clean-arch

3
11
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
3
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?