← しきぴょんたの部屋 / Tools 📝 制作記 ▶ ビジュアライザー 📐 見積もり
⚡ Pareto Principle × Algorithms — v2

Web系エンジニアの業務
8割をカバーする
アルゴリズム

10個のアルゴリズムを深く理解すれば、日常業務の80%は対応できる。
PHP / TypeScript / Vue.js 実装 + 業務システム頻出パターン集。

なぜこの10個なのか?

Web系システム開発の業務を分解すると、大半は以下のパターンに帰着する。

業務カテゴリ頻度対応アルゴリズム
データの保存・取得・表示★★★★★ハッシュマップ、CRUD操作
一覧表示・検索・絞り込み★★★★★ソート、二分探索、フィルタリング
階層データの処理★★★★☆木構造探索(DFS/BFS)
バッチ処理・非同期処理★★★★☆キュー / スタック
パフォーマンス最適化★★★☆☆キャッシュ(LRU)、メモ化
入力検証・テキスト処理★★★★☆正規表現、文字列操作
ページネーション★★★★☆オフセット / カーソルベース
依存関係・実行順序の管理★★★☆☆トポロジカルソート
重複排除・集合演算★★★★☆Set / Map 操作
状態管理・ワークフロー★★★☆☆ステートマシン
01

ハッシュマップ(連想配列)

すべての基盤 — O(1) でデータを引く力
なぜ核心か? Web開発の9割は「キーで値を引く」操作。DBレコード、APIレスポンス、セッション管理…すべて連想配列に帰着する。
IDインデックス化 APIレスポンス整形 マスタデータ参照 GROUP BY実装
// DBレコードをIDでインデックス化 — O(n)構築 → O(1)参照
function indexById(array $records, string $key = 'id'): array
{
    $indexed = [];
    foreach ($records as $record) {
        $indexed[$record[$key]] = $record;
    }
    return $indexed;
}

// 応用:GROUP BY のアプリケーション層実装
function groupBy(array $records, string $key): array
{
    $grouped = [];
    foreach ($records as $record) {
        $grouped[$record[$key]][] = $record;
    }
    return $grouped;
}
// ジェネリック型で型安全なインデックス化
function indexBy<T, K extends keyof T>(
  items: T[],
  key: K
): Map<T[K], T> {
  return new Map(items.map(item => [item[key], item]))
}

// ジェネリック GROUP BY
function groupBy<T, K extends keyof T>(
  items: T[],
  key: K
): Map<T[K], T[]> {
  const map = new Map<T[K], T[]>()
  for (const item of items) {
    const group = map.get(item[key]) ?? []
    group.push(item)
    map.set(item[key], group)
  }
  return map
}

// 使用例
interface User {
  id: number
  name: string
  dept: string
}
const users: User[] = [
  { id: 1, name: '田中太郎', dept: 'engineering' },
  { id: 2, name: '佐藤花子', dept: 'design' },
]

const userMap = indexBy(users, 'id')  // Map<number, User>
userMap.get(2)?.name  // "佐藤花子" — 型安全 + O(1)

const byDept = groupBy(users, 'dept') // Map<string, User[]>
// Vue 3 Composition API でのルックアップテーブル
import { ref, computed } from 'vue'

const users = ref([
  { id: 1, name: '田中太郎' },
  { id: 2, name: '佐藤花子' },
])

// computed で Map 構築 → テンプレートから O(1) 参照
const userMap = computed(() =>
  new Map(users.value.map(u => [u.id, u]))
)
02

ソートとフィルタリング

一覧画面の生命線 — 動的な条件組み立て
なぜ核心か? 管理画面、検索結果、ダッシュボード…一覧表示がない業務システムは存在しない。
管理画面一覧検索結果ソート動的フィルタ
// 多条件ソート + パイプラインフィルタ
function multiSort(array &$records, array $criteria): void
{
    usort($records, function ($a, $b) use ($criteria) {
        foreach ($criteria as [$key, $dir]) {
            $cmp = $a[$key] <=> $b[$key];
            if ($cmp !== 0) return $dir === 'desc' ? -$cmp : $cmp;
        }
        return 0;
    });
}

function applyFilters(array $records, array $filters): array
{
    return array_values(array_filter($records,
        fn($r) => array_all($filters, fn($f) => $f($r))
    ));
}
// 型安全な多条件ソート
type SortCriteria<T> = {
  key: keyof T
  dir: 'asc' | 'desc'
}[]

function multiSort<T>(items: T[], criteria: SortCriteria<T>): T[] {
  return [...items].sort((a, b) => {
    for (const { key, dir } of criteria) {
      const av = a[key], bv = b[key]
      if (av < bv) return dir === 'asc' ? -1 : 1
      if (av > bv) return dir === 'asc' ? 1 : -1
    }
    return 0
  })
}

// パイプラインフィルタ(型推論で条件を型安全に)
type FilterFn<T> = (item: T) => boolean

function applyFilters<T>(items: T[], filters: FilterFn<T>[]): T[] {
  return items.filter(item =>
    filters.every(f => f(item))
  )
}

// 使用例:動的フィルタ構築
interface Order { date: string; amount: number; customer: string }
const filters: FilterFn<Order>[] = []
if (minAmount) filters.push(o => o.amount >= minAmount)
if (dateFrom)  filters.push(o => o.date >= dateFrom)
const result = applyFilters(orders, filters)
03

木構造の探索(DFS / BFS)

階層データの支配者
なぜ核心か? カテゴリツリー、組織図、メニュー構造、コメントスレッド…「親子関係」データは至るところに存在する。
カテゴリツリーパンくず組織図コメントスレッド
// フラット配列 → ツリー構造(adjacency list → JSON tree)
function buildTree(array $items, ?int $parentId = null): array
{
    $tree = [];
    foreach ($items as $item) {
        if ($item['parent_id'] === $parentId) {
            $children = buildTree($items, $item['id']);
            if ($children) $item['children'] = $children;
            $tree[] = $item;
        }
    }
    return $tree;
}

// パンくず生成(ルートまで遡る)
function findPathToNode(array $items, int $targetId): array
{
    $map = array_column($items, null, 'id');
    $path = [];
    $cur = $targetId;
    while ($cur !== null) {
        array_unshift($path, $map[$cur]);
        $cur = $map[$cur]['parent_id'];
    }
    return $path;
}
// 再帰的ツリー型定義
interface TreeNode<T> {
  id: number
  parentId: number | null
  children?: TreeNode<T>[]
  data: T
}

// O(n) で構築するツリービルダー(再帰より高速)
function buildTree<T extends { id: number; parentId: number | null }>(
  items: T[]
): TreeNode<T>[] {
  const map = new Map<number, TreeNode<T>>()
  const roots: TreeNode<T>[] = []

  // 1パス目: 全ノードを Map に登録
  for (const item of items) {
    map.set(item.id, { ...item, data: item, children: [] })
  }

  // 2パス目: 親子関係を構築
  for (const node of map.values()) {
    if (node.parentId === null) {
      roots.push(node)
    } else {
      map.get(node.parentId)?.children!.push(node)
    }
  }
  return roots
}

// パンくずリスト生成
function getBreadcrumb<T extends { id: number; parentId: number | null }>(
  items: T[], targetId: number
): T[] {
  const map = new Map(items.map(i => [i.id, i]))
  const path: T[] = []
  let cur: number | null = targetId
  while (cur !== null) {
    const node = map.get(cur)!
    path.unshift(node)
    cur = node.parentId
  }
  return path
}
04

キュー / スタック

非同期処理の要 — FIFO と LIFO
なぜ核心か? メール送信、PDF生成、画像リサイズ…即時レスポンス不要な処理はすべてキューに投入。Undo機能はスタック。
ジョブキューバッチ処理Undo/RedoBFS探索
class SimpleJobQueue
{
    private array $queue = [];
    public function enqueue(string $type, array $payload): void
    {   $this->queue[] = ['type' => $type, 'payload' => $payload];   }

    public function process(): void
    {
        while ($job = array_shift($this->queue)) { // FIFO
            match ($job['type']) {
                'send_email'   => $this->handleEmail($job['payload']),
                'generate_pdf' => $this->handlePdf($job['payload']),
            };
        }
    }
}
// 型安全なジョブキュー(Discriminated Union)
type Job =
  | { type: 'send_email';   payload: { to: string; template: string } }
  | { type: 'generate_pdf'; payload: { invoiceId: number } }
  | { type: 'resize_image'; payload: { path: string; width: number } }

class JobQueue {
  private queue: Job[] = []

  enqueue(job: Job): void { this.queue.push(job) }

  async process(): Promise<void> {
    let job: Job | undefined
    while (job = this.queue.shift()) {
      switch (job.type) {
        case 'send_email':
          await sendEmail(job.payload.to)     // 型推論で to が確定
          break
        case 'generate_pdf':
          await generatePdf(job.payload.invoiceId) // 型推論で invoiceId 確定
          break
        case 'resize_image':
          await resizeImage(job.payload.path, job.payload.width)
          break
      }
    }
  }
}

// Undo スタック with ジェネリクス
class UndoStack<T> {
  private stack: T[] = []
  push(state: T): void { this.stack.push(state) }
  pop(): T | undefined { return this.stack.pop() }      // LIFO
  get canUndo(): boolean { return this.stack.length > 0 }
}
05

キャッシュとメモ化

パフォーマンスの切り札
なぜ核心か? 「遅い」→ まずキャッシュを疑え。DBクエリ、API呼び出しの結果をキャッシュするだけで劇的改善。
LRUキャッシュ関数メモ化APIキャッシュ
// メモ化:高コスト関数の結果を自動キャッシュ
function memoize(callable $fn): Closure
{
    $cache = [];
    return function () use ($fn, &$cache) {
        $key = serialize(func_get_args());
        return $cache[$key] ??= $fn(...func_get_args());
    };
}
// 型安全な LRU キャッシュ
class LRUCache<K, V> {
  private cache = new Map<K, V>()
  constructor(private capacity: number) {}

  get(key: K): V | undefined {
    if (!this.cache.has(key)) return undefined
    const val = this.cache.get(key)!
    this.cache.delete(key)
    this.cache.set(key, val)  // 最新に移動
    return val
  }

  set(key: K, value: V): void {
    this.cache.delete(key)
    if (this.cache.size >= this.capacity) {
      const oldest = this.cache.keys().next().value
      this.cache.delete(oldest)  // LRU 削除
    }
    this.cache.set(key, value)
  }
}

// 型安全なメモ化デコレータ
function memoize<Args extends unknown[], R>(
  fn: (...args: Args) => R
): (...args: Args) => R {
  const cache = new Map<string, R>()
  return (...args) => {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)!
    const result = fn(...args)
    cache.set(key, result)
    return result
  }
}
06

文字列操作と正規表現

バリデーションの守護者
なぜ核心か? 入力検証、データクレンジング、テンプレート展開、ログ解析…文字列を扱わない日はない。
電話番号郵便番号テンプレートカタカナ検証
class Validator
{
    public static function isPhoneNumber(string $v): bool
    {   return (bool)preg_match('/\A0\d{1,4}-?\d{1,4}-?\d{3,4}\z/', $v);   }

    public static function isPostalCode(string $v): bool
    {   return (bool)preg_match('/\A\d{3}-?\d{4}\z/', $v);   }
}

function renderTemplate(string $tpl, array $vars): string
{
    return preg_replace_callback('/\{\{(\w+)\}\}/', function ($m) use ($vars) {
        return htmlspecialchars($vars[$m[1]] ?? '', ENT_QUOTES, 'UTF-8');
    }, $tpl);
}
// Branded Types で検証済みの値を型レベルで保証
type Brand<T, B> = T & { readonly __brand: B }
type Email       = Brand<string, 'Email'>
type PhoneNumber = Brand<string, 'PhoneNumber'>
type PostalCode  = Brand<string, 'PostalCode'>

const patterns = {
  email:  /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  phone:  /^0\d{1,4}-?\d{1,4}-?\d{3,4}$/,
  postal: /^\d{3}-?\d{4}$/,
  katakana: /^[ァ-ヶー]+$/u,
} as const

function parseEmail(v: string): Email | null {
  return patterns.email.test(v) ? v as Email : null
}

function parsePhoneNumber(v: string): PhoneNumber | null {
  return patterns.phone.test(v) ? v as PhoneNumber : null
}

// 型安全テンプレート
function render<T extends Record<string, string>>(
  tpl: string, vars: T
): string {
  return tpl.replace(/\{\{(\w+)\}\}/g,
    (_, k) => vars[k] ?? '')
}

// 使用例: sendEmail は Email 型のみ受付
function sendEmail(to: Email, body: string): void { /* ... */ }
const email = parseEmail(input)
if (email) sendEmail(email, 'Hello') // ✅ コンパイル通る
// sendEmail('bad', 'Hello')          // ❌ コンパイルエラー
07

ページネーション

大量データ表示 — Offset vs Cursor
なぜ核心か? 全件取得は絶対NG。オフセット方式(ページ番号指定可能)とカーソル方式(高速、無限スクロール向き)を使い分ける。
一覧ページ無限スクロールAPIページネーション
// カーソルベース・ページネーションの型定義
interface CursorPage<T> {
  data: T[]
  nextCursor: string | null
  hasMore: boolean
}

interface OffsetPage<T> {
  data: T[]
  meta: {
    currentPage: number
    perPage: number
    totalItems: number
    totalPages: number
  }
}

// カーソルベースのレスポンスビルダー
function buildCursorPage<T extends { id: string }>(
  items: T[], limit: number
): CursorPage<T> {
  const hasMore = items.length > limit
  const data = hasMore ? items.slice(0, limit) : items
  return {
    data,
    nextCursor: hasMore ? data.at(-1)!.id : null,
    hasMore,
  }
}

// フロント:無限スクロール用 composable
async function useCursorPagination<T>(
  fetcher: (cursor?: string) => Promise<CursorPage<T>>
) {
  let cursor: string | undefined
  let hasMore = true
  const items: T[] = []

  return {
    items,
    async loadMore() {
      if (!hasMore) return
      const page = await fetcher(cursor)
      items.push(...page.data)
      cursor = page.nextCursor ?? undefined
      hasMore = page.hasMore
    },
    get hasMore() { return hasMore }
  }
}
08

トポロジカルソート

依存関係解決の決定打
なぜ核心か? マイグレーション順序、タスク実行順、ビルド順序…「AをBの前に」という制約の整理に不可欠。
// カーンのアルゴリズム(TypeScript 版)
// graph: ノード → 依存先(そのノードより先に実行すべきもの)
function topologicalSort(graph: Map<string, string[]>): string[] {
  // 未解決の依存数と、「自分に依存しているノード」の逆引きを作る
  const inDegree = new Map<string, number>()
  const dependents = new Map<string, string[]>()

  for (const [node, deps] of graph) {
    inDegree.set(node, deps.length)
    for (const dep of deps) {
      const list = dependents.get(dep) ?? []
      list.push(node)
      dependents.set(dep, list)
    }
  }

  // 依存ゼロ(すぐ実行できる)ノードからスタート
  const queue = [...inDegree.entries()]
    .filter(([, d]) => d === 0).map(([n]) => n)
  const sorted: string[] = []

  while (queue.length) {
    const node = queue.shift()!
    sorted.push(node)
    // 完了したので、自分を待っていたノードの依存数を減らす
    for (const dependent of dependents.get(node) ?? []) {
      const d = inDegree.get(dependent)! - 1
      inDegree.set(dependent, d)
      if (d === 0) queue.push(dependent)
    }
  }
  if (sorted.length !== graph.size)
    throw new Error('循環依存を検出')
  return sorted
}
09

集合演算(Set / Map)

データの突合・重複排除
なぜ核心か? 「AにあってBにないデータ」「両方に存在するデータ」「重複排除」は日常茶飯事。
// ユーティリティ関数群
const union      = <T>(a: Set<T>, b: Set<T>): Set<T> =>
  new Set([...a, ...b])
const intersect  = <T>(a: Set<T>, b: Set<T>): Set<T> =>
  new Set([...a].filter(x => b.has(x)))
const diff       = <T>(a: Set<T>, b: Set<T>): Set<T> =>
  new Set([...a].filter(x => !b.has(x)))

// 実務応用:2つのデータセットの差分検出
interface DiffResult<T> {
  added:   T[]
  removed: T[]
  changed: { old: T; new: T }[]
}

function diffBy<T, K>(
  oldItems: T[], newItems: T[],
  keyFn: (item: T) => K,
  eqFn: (a: T, b: T) => boolean = (a, b) => JSON.stringify(a) === JSON.stringify(b)
): DiffResult<T> {
  const oldMap = new Map(oldItems.map(i => [keyFn(i), i]))
  const newMap = new Map(newItems.map(i => [keyFn(i), i]))

  return {
    added:   newItems.filter(i => !oldMap.has(keyFn(i))),
    removed: oldItems.filter(i => !newMap.has(keyFn(i))),
    changed: newItems
      .filter(n => {
        const o = oldMap.get(keyFn(n))
        return o && !eqFn(o, n)
      })
      .map(n => ({ old: oldMap.get(keyFn(n))!, new: n }))
  }
}
10

ステートマシン

ワークフローの設計図
なぜ核心か? 注文ステータス、承認フロー、画面遷移…「状態」と「遷移」を一元管理すれば複雑なロジックも明快に。
// 型レベルで不正遷移を防ぐステートマシン
type OrderStatus =
  | 'pending' | 'confirmed' | 'processing'
  | 'shipped' | 'delivered' | 'cancelled' | 'returned'

type Transitions = {
  pending:    ['confirmed', 'cancelled']
  confirmed:  ['processing', 'cancelled']
  processing: ['shipped', 'cancelled']
  shipped:    ['delivered', 'returned']
  delivered:  ['returned']
  cancelled:  []
  returned:   []
}

class OrderStateMachine {
  private static readonly transitions: Record<OrderStatus, OrderStatus[]> = {
    pending:    ['confirmed', 'cancelled'],
    confirmed:  ['processing', 'cancelled'],
    processing: ['shipped', 'cancelled'],
    shipped:    ['delivered', 'returned'],
    delivered:  ['returned'],
    cancelled:  [],
    returned:   [],
  }

  constructor(private state: OrderStatus = 'pending') {}

  transition(to: OrderStatus): void {
    const allowed = OrderStateMachine.transitions[this.state]
    if (!allowed.includes(to))
      throw new Error(`遷移不可: ${this.state} → ${to}`)
    this.state = to
  }

  get current(): OrderStatus { return this.state }
  get available(): OrderStatus[] {
    return OrderStateMachine.transitions[this.state]
  }
}

🏭 業務システム頻出パターン

アルゴリズムが「部品」なら、以下のパターンは「設計図」。現場で繰り返し登場する構造を TypeScript で実装例を示す。

📦

Repository パターン

データアクセスをビジネスロジックから分離する
DB操作を抽象化し、テスト可能にする最も基本的なパターン。CakePHP の Table クラスも本質的にこれ。
✅ こんな時に使うDB差し替え可能にしたい時、単体テストでDBモックが必要な時、複数データソース統合時
⚠️ 注意点CakePHP では Table + Entity が近い役割を果たすので過剰な抽象化に注意
// ジェネリック Repository インターフェース
interface Repository<T, ID = number> {
  findById(id: ID): Promise<T | null>
  findAll(filter?: Partial<T>): Promise<T[]>
  create(data: Omit<T, 'id'>): Promise<T>
  update(id: ID, data: Partial<T>): Promise<T>
  delete(id: ID): Promise<void>
}

// 具象実装例(Prisma 想定)
class UserRepository implements Repository<User> {
  constructor(private prisma: PrismaClient) {}

  findById(id: number) {
    return this.prisma.user.findUnique({ where: { id } })
  }
  findAll(filter?: Partial<User>) {
    return this.prisma.user.findMany({ where: filter })
  }
  // ...create, update, delete
}

// テスト時はモック実装を注入
class MockUserRepository implements Repository<User> {
  private store = new Map<number, User>()
  // ... インメモリ実装
}
// CakePHP の Table クラスをラップした Repository
interface RepositoryInterface
{
    public function findById(int $id): ?EntityInterface;
    public function findAll(array $conditions = []): array;
    public function save(EntityInterface $entity): EntityInterface;
    public function delete(int $id): bool;
}

// CakePHP 実装
class UserRepository implements RepositoryInterface
{
    public function __construct(
        private UsersTable $table
    ) {}

    public function findById(int $id): ?User
    {
        return $this->table->find()
            ->where(['id' => $id])
            ->first();
    }

    public function findAll(array $conditions = []): array
    {
        return $this->table->find()
            ->where($conditions)
            ->toArray();
    }

    public function save(EntityInterface $entity): User
    {
        $result = $this->table->save($entity);
        if (!$result) {
            throw new \RuntimeException('保存に失敗しました');
        }
        return $result;
    }

    // テスト用: 条件付き検索を Repository に集約
    public function findActiveByDept(string $dept): array
    {
        return $this->table->find()
            ->where(['dept' => $dept, 'is_active' => true])
            ->order(['name' => 'ASC'])
            ->toArray();
    }
}
// フロント向け: API クライアントを Repository として抽象化
class UserApiRepository {
  constructor(baseUrl = '/api/users') {
    this.baseUrl = baseUrl
  }

  async findById(id) {
    const res = await fetch(`${this.baseUrl}/${id}`)
    if (!res.ok) return null
    return res.json()
  }

  async findAll(params = {}) {
    const qs = new URLSearchParams(params)
    const res = await fetch(`${this.baseUrl}?${qs}`)
    return res.json()
  }

  async create(data) {
    const res = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    if (!res.ok) throw new Error(await res.text())
    return res.json()
  }

  async update(id, data) {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    return res.json()
  }
}

// Vue 3 での使用例
const userRepo = new UserApiRepository()
const users = ref([])

onMounted(async () => {
  users.value = await userRepo.findAll({ dept: 'engineering' })
})
🛡️

DTO + バリデーション

外部入力を安全な型に変換するゲートキーパー
APIリクエスト・フォーム入力を受け取る際、未検証の any/unknown を信頼できる型に変換する。Zodが事実上の標準。
✅ こんな時に使うAPIリクエスト受付、フォーム入力処理、外部APIレスポンス受信、CSV取込み
⚠️ 注意点内部のドメインオブジェクト間の変換には過剰。信頼境界(Trust Boundary)で使う
import { z } from 'zod'

// スキーマ定義 = バリデーション + 型生成を1箇所で
const CreateOrderSchema = z.object({
  customerId: z.number().int().positive(),
  items: z.array(z.object({
    productId: z.number().int().positive(),
    quantity:  z.number().int().min(1).max(99),
  })).min(1),
  note: z.string().max(500).optional(),
  deliveryDate: z.coerce.date().min(new Date()),
})

// スキーマから型を自動生成(DRY)
type CreateOrderDTO = z.infer<typeof CreateOrderSchema>

// API ハンドラで使用
async function handleCreateOrder(req: Request): Promise<Response> {
  const result = CreateOrderSchema.safeParse(req.body)

  if (!result.success) {
    // Zod が構造化されたエラーを返す
    return Response.json({
      errors: result.error.flatten().fieldErrors
    }, { status: 422 })
  }

  // result.data は CreateOrderDTO 型として確定 ✅
  const order = await orderService.create(result.data)
  return Response.json(order, { status: 201 })
}
// バリデーション + DTO 変換クラス(CakePHP 風)
class CreateOrderDTO
{
    public function __construct(
        public readonly int    $customerId,
        public readonly array  $items,
        public readonly ?string $note,
        public readonly DateTimeImmutable $deliveryDate,
    ) {}

    /**
     * リクエストデータからDTOを生成(バリデーション込み)
     * @throws ValidationException
     */
    public static function fromRequest(array $data): self
    {
        $errors = [];

        // customerId
        if (!isset($data['customer_id']) || !is_int($data['customer_id'])) {
            $errors['customer_id'] = '正の整数が必要です';
        }

        // items
        if (empty($data['items']) || !is_array($data['items'])) {
            $errors['items'] = '1件以上の商品が必要です';
        } else {
            foreach ($data['items'] as $i => $item) {
                if (($item['quantity'] ?? 0) < 1 || ($item['quantity'] ?? 0) > 99) {
                    $errors["items.{$i}.quantity"] = '1〜99の範囲で指定';
                }
            }
        }

        // note
        if (isset($data['note']) && mb_strlen($data['note']) > 500) {
            $errors['note'] = '500文字以内で入力してください';
        }

        if (!empty($errors)) {
            throw new ValidationException($errors);
        }

        return new self(
            customerId:   (int)$data['customer_id'],
            items:        $data['items'],
            note:         $data['note'] ?? null,
            deliveryDate: new DateTimeImmutable($data['delivery_date']),
        );
    }
}

// Controller での使用
public function add(): void
{
    try {
        $dto = CreateOrderDTO::fromRequest($this->request->getData());
        $order = $this->orderService->create($dto);
        $this->set('order', $order);
    } catch (ValidationException $e) {
        $this->response = $this->response->withStatus(422);
        $this->set('errors', $e->getErrors());
    }
}
// フロント側:送信前のバリデーション(Vue 3 composable)
function useOrderValidation() {
  const errors = ref({})

  const rules = {
    customerId: (v) => {
      if (!v || v <= 0) return '顧客IDは必須です'
      return null
    },
    items: (v) => {
      if (!Array.isArray(v) || v.length === 0) return '商品を1つ以上追加してください'
      for (const item of v) {
        if (item.quantity < 1 || item.quantity > 99)
          return '数量は1〜99の範囲で指定'
      }
      return null
    },
    note: (v) => {
      if (v && v.length > 500) return '500文字以内で入力してください'
      return null
    },
  }

  function validate(data) {
    const newErrors = {}
    for (const [field, rule] of Object.entries(rules)) {
      const error = rule(data[field])
      if (error) newErrors[field] = error
    }
    errors.value = newErrors
    return Object.keys(newErrors).length === 0
  }

  // API エラーレスポンスを errors にマージ
  function setServerErrors(serverErrors) {
    errors.value = { ...errors.value, ...serverErrors }
  }

  return { errors, validate, setServerErrors }
}

// コンポーネントでの使用
const { errors, validate, setServerErrors } = useOrderValidation()

async function handleSubmit() {
  if (!validate(formData.value)) return // フロント検証

  const res = await fetch('/api/orders', { /* ... */ })
  if (res.status === 422) {
    const { errors: srvErr } = await res.json()
    setServerErrors(srvErr) // サーバー検証エラーを反映
  }
}
⚙️

Service Layer パターン

ビジネスロジックの司令塔
Controller(ルーティング)と Repository(データアクセス)の間でビジネスルールを管理する。「太ったコントローラ」を防ぐ鍵。
✅ こんな時に使う複数テーブルにまたがる処理、トランザクション管理、外部サービス連携
⚠️ 注意点単純な CRUD は Service を経由せず直接 Repository でもOK(YAGNI)
class OrderService {
  constructor(
    private orderRepo: Repository<Order>,
    private stockRepo: Repository<Stock>,
    private mailer: MailService,
    private db: TransactionManager,
  ) {}

  // ビジネスルール:注文作成
  async createOrder(dto: CreateOrderDTO): Promise<Order> {
    return this.db.transaction(async () => {
      // 1. 在庫チェック
      for (const item of dto.items) {
        const stock = await this.stockRepo.findById(item.productId)
        if (!stock || stock.quantity < item.quantity) {
          throw new InsufficientStockError(item.productId)
        }
      }

      // 2. 注文作成
      const order = await this.orderRepo.create({
        customerId: dto.customerId,
        status: 'pending',
        items: dto.items,
      })

      // 3. 在庫引当
      for (const item of dto.items) {
        await this.stockRepo.update(item.productId, {
          quantity: { decrement: item.quantity }
        })
      }

      // 4. 確認メール(キューイング)
      await this.mailer.enqueue('order_confirmation', { orderId: order.id })

      return order
    })
  }
}
// CakePHP でのサービスクラス
class OrderService
{
    public function __construct(
        private OrdersTable $orders,
        private StocksTable $stocks,
        private MailerFactory $mailer,
    ) {}

    /**
     * 注文作成:在庫チェック → 注文保存 → 在庫引当 → メール送信
     * @throws InsufficientStockException
     */
    public function createOrder(CreateOrderDTO $dto): Order
    {
        // トランザクションで一括管理
        return $this->orders->getConnection()->transactional(
            function () use ($dto) {
                // 1. 在庫チェック
                foreach ($dto->items as $item) {
                    $stock = $this->stocks->get($item['product_id']);
                    if ($stock->quantity < $item['quantity']) {
                        throw new InsufficientStockException(
                            $item['product_id']
                        );
                    }
                }

                // 2. 注文エンティティ作成 + 保存
                $order = $this->orders->newEntity([
                    'customer_id' => $dto->customerId,
                    'status'      => 'pending',
                    'items'       => $dto->items,
                ]);
                $this->orders->saveOrFail($order);

                // 3. 在庫引当
                foreach ($dto->items as $item) {
                    $this->stocks->decrement(
                        $item['product_id'], $item['quantity']
                    );
                }

                // 4. メール送信(キューに投入)
                $this->mailer->create('OrderConfirmation')
                    ->send('confirm', [$order]);

                return $order;
            }
        );
    }
}

// Controller は薄く保つ
class OrdersController extends AppController
{
    public function add(): void
    {
        $dto = CreateOrderDTO::fromRequest($this->request->getData());
        $order = $this->orderService->createOrder($dto);
        $this->set(compact('order'));
    }
}
// フロント: API呼び出しのビジネスロジックを集約
class OrderService {
  constructor(orderRepo, notifier) {
    this.orderRepo = orderRepo
    this.notifier = notifier
  }

  /**
   * 注文フロー全体をオーケストレーション
   * UI層はこのメソッドを呼ぶだけでよい
   */
  async submitOrder(formData) {
    // 1. 楽観的UI更新(即座に一覧に反映)
    const tempId = `temp_${Date.now()}`
    this.notifier.emit('order:optimistic-add', {
      id: tempId, ...formData, status: 'pending'
    })

    try {
      // 2. API送信
      const order = await this.orderRepo.create(formData)

      // 3. 楽観的データを実データで置換
      this.notifier.emit('order:confirmed', { tempId, order })

      // 4. 成功通知
      this.notifier.emit('toast:show', {
        type: 'success', message: '注文が完了しました'
      })

      return order
    } catch (error) {
      // 5. 楽観的更新をロールバック
      this.notifier.emit('order:rollback', { tempId })
      throw error
    }
  }
}
🎯

Result 型パターン

例外を投げずにエラーを型で表現する
try-catch の乱立を防ぎ、「この関数は失敗しうる」を型シグネチャで明示する。Go言語の error returns に近い発想。
✅ こんな時に使うバリデーション、ビジネスルール違反、外部API呼び出し、ファイル処理
⚠️ 注意点回復不能なエラー(DB接続断など)は素直に throw する
// Result 型定義
type Result<T, E = Error> =
  | { ok: true;  value: T }
  | { ok: false; error: E }

// ヘルパー関数
const Ok  = <T>(value: T): Result<T, never> =>
  ({ ok: true, value })
const Err = <E>(error: E): Result<never, E> =>
  ({ ok: false, error })

// ビジネスエラーを型で表現
type OrderError =
  | { code: 'INSUFFICIENT_STOCK'; productId: number }
  | { code: 'CUSTOMER_NOT_FOUND'; customerId: number }
  | { code: 'DAILY_LIMIT_EXCEEDED'; limit: number }

async function createOrder(
  dto: CreateOrderDTO
): Promise<Result<Order, OrderError>> {
  const customer = await customerRepo.findById(dto.customerId)
  if (!customer)
    return Err({ code: 'CUSTOMER_NOT_FOUND', customerId: dto.customerId })

  // ... 在庫チェック、上限チェック
  const order = await orderRepo.create(dto)
  return Ok(order)
}

// 呼び出し側:パターンマッチで安全に処理
const result = await createOrder(dto)
if (result.ok) {
  console.log('注文完了:', result.value.id)  // 型: Order
} else {
  switch (result.error.code) {
    case 'INSUFFICIENT_STOCK':
      console.log(`在庫不足: 商品ID ${result.error.productId}`)
      break
    case 'DAILY_LIMIT_EXCEEDED':
      console.log(`上限超過: ${result.error.limit}件/日`)
      break
  }
}
// PHP 版 Result クラス(readonly + enum で堅牢に)
class Result
{
    private function __construct(
        public readonly bool   $ok,
        public readonly mixed  $value = null,
        public readonly ?array $error = null,
    ) {}

    public static function ok(mixed $value): self
    {
        return new self(ok: true, value: $value);
    }

    public static function fail(string $code, array $context = []): self
    {
        return new self(ok: false, error: ['code' => $code, ...$context]);
    }
}

// Service での使用
class OrderService
{
    public function createOrder(CreateOrderDTO $dto): Result
    {
        $customer = $this->customers->get($dto->customerId);
        if (!$customer) {
            return Result::fail('CUSTOMER_NOT_FOUND', [
                'customerId' => $dto->customerId,
            ]);
        }

        foreach ($dto->items as $item) {
            $stock = $this->stocks->get($item['product_id']);
            if ($stock->quantity < $item['quantity']) {
                return Result::fail('INSUFFICIENT_STOCK', [
                    'productId' => $item['product_id'],
                ]);
            }
        }

        $order = $this->orders->saveOrFail(/* ... */);
        return Result::ok($order);
    }
}

// Controller でパターンマッチ
$result = $this->orderService->createOrder($dto);
if ($result->ok) {
    $this->set('order', $result->value);
} else {
    match ($result->error['code']) {
        'CUSTOMER_NOT_FOUND'  => $this->Flash->error('顧客が見つかりません'),
        'INSUFFICIENT_STOCK' => $this->Flash->error('在庫不足です'),
    };
}
// JavaScript 版 Result パターン(フロント向け)
const Ok  = (value) => ({ ok: true, value })
const Err = (error) => ({ ok: false, error })

// API呼び出しを Result でラップ
async function safeApiCall(url, options = {}) {
  try {
    const res = await fetch(url, {
      headers: { 'Content-Type': 'application/json' },
      ...options,
    })

    if (!res.ok) {
      const body = await res.json().catch(() => ({}))
      return Err({
        code: body.code ?? 'API_ERROR',
        status: res.status,
        ...body,
      })
    }

    return Ok(await res.json())
  } catch (e) {
    return Err({ code: 'NETWORK_ERROR', message: e.message })
  }
}

// Vue コンポーネントでの使用
async function handleCreateOrder() {
  loading.value = true
  const result = await safeApiCall('/api/orders', {
    method: 'POST',
    body: JSON.stringify(formData.value),
  })

  if (result.ok) {
    router.push(`/orders/${result.value.id}`)
  } else {
    switch (result.error.code) {
      case 'INSUFFICIENT_STOCK':
        toast.error('在庫不足です'); break
      case 'NETWORK_ERROR':
        toast.error('通信エラー。再試行してください'); break
      default:
        toast.error('予期しないエラーが発生しました')
    }
  }
  loading.value = false
}
📡

イベント駆動 / Observer パターン

処理を疎結合に連鎖させる
「注文が作成された → メール送信 + 在庫更新 + ログ記録」のように、1つのアクションに複数の副作用を疎結合に紐づける。
✅ こんな時に使う副作用の追加が頻繁、処理の独立性が高い、テスタビリティ重視
⚠️ 注意点イベントの連鎖が深くなると追跡困難になる。3段階以上は要注意
// 型安全な EventEmitter
type EventMap = {
  'order.created':   { orderId: number; customerId: number }
  'order.shipped':   { orderId: number; trackingNo: string }
  'order.cancelled': { orderId: number; reason: string }
  'user.registered': { userId: number; email: string }
}

class TypedEventBus {
  private handlers = new Map<string, Function[]>()

  on<K extends keyof EventMap>(
    event: K,
    handler: (payload: EventMap[K]) => void | Promise<void>
  ): void {
    const list = this.handlers.get(event) ?? []
    list.push(handler)
    this.handlers.set(event, list)
  }

  async emit<K extends keyof EventMap>(
    event: K, payload: EventMap[K]
  ): Promise<void> {
    const handlers = this.handlers.get(event) ?? []
    await Promise.allSettled(handlers.map(h => h(payload)))
  }
}

// 使用例:各リスナーが独立して追加・削除可能
const bus = new TypedEventBus()
bus.on('order.created', async (e) => sendEmail(e.customerId))
bus.on('order.created', async (e) => updateDashboard(e.orderId))
bus.on('order.created', async (e) => writeAuditLog('order.created', e))
// PHP 版 シンプル EventDispatcher
class EventDispatcher
{
    private array $listeners = [];

    public function on(string $event, callable $handler): void
    {
        $this->listeners[$event][] = $handler;
    }

    public function dispatch(string $event, array $payload = []): void
    {
        foreach ($this->listeners[$event] ?? [] as $handler) {
            $handler($payload);
        }
    }
}

// イベントクラスで構造化(PSR-14 的アプローチ)
class OrderCreatedEvent
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $customerId,
    ) {}
}

// リスナー登録(bootstrap 等で)
$dispatcher = new EventDispatcher();

$dispatcher->on('order.created', function ($e) {
    // 確認メール送信
    $mailer = new OrderConfirmationMailer();
    $mailer->send($e['customerId']);
});

$dispatcher->on('order.created', function ($e) {
    // 監査ログ記録
    AuditLog::write('order.created', $e);
});

$dispatcher->on('order.created', function ($e) {
    // Slack 通知
    SlackNotifier::notify("#orders", "新規注文: #{$e['orderId']}");
});

// Service 内で発火
$dispatcher->dispatch('order.created', [
    'orderId'    => $order->id,
    'customerId' => $order->customer_id,
]);
// フロント向け: mitt ライクな軽量 EventBus
function createEventBus() {
  const handlers = new Map()

  return {
    on(event, handler) {
      const list = handlers.get(event) ?? []
      list.push(handler)
      handlers.set(event, list)
      return () => { // unsubscribe 関数を返す
        const i = list.indexOf(handler)
        if (i > -1) list.splice(i, 1)
      }
    },
    emit(event, payload) {
      (handlers.get(event) ?? []).forEach(h => h(payload))
    },
  }
}

// Vue 3 の provide/inject と組み合わせ
// main.js
const bus = createEventBus()
app.provide('eventBus', bus)

// OrderForm.vue
const bus = inject('eventBus')
async function onSubmit() {
  const order = await api.createOrder(form)
  bus.emit('order:created', order)
}

// OrderList.vue(別コンポーネントでリアクティブに反映)
const bus = inject('eventBus')
onMounted(() => {
  const unsub = bus.on('order:created', (order) => {
    orders.value.unshift(order)
  })
  onUnmounted(unsub) // メモリリーク防止
})
🔒

楽観的ロック

同時更新の競合を安全に検出する
「管理画面で2人が同時に同じレコードを編集して、先に保存した人の変更が消える」問題を防ぐ。version カラムで更新競合を検出する。
✅ こんな時に使う管理画面のレコード編集、在庫数の更新、設定値の変更
⚠️ 注意点高頻度更新(秒単位)にはDB側の排他ロック(SELECT FOR UPDATE)の方が適切
// version フィールドによる楽観的ロック
interface Versioned {
  id: number
  version: number
}

class OptimisticLockError extends Error {
  constructor(entity: string, id: number) {
    super(`${entity} #${id} は他のユーザーに更新されました`)
  }
}

async function updateWithLock<T extends Versioned>(
  table: string,
  id: number,
  currentVersion: number,
  updates: Partial<T>
): Promise<T> {
  // WHERE id = ? AND version = ? で更新
  const result = await db.query(`
    UPDATE ${table}
    SET ..., version = version + 1
    WHERE id = $1 AND version = $2
    RETURNING *
  `, [id, currentVersion])

  // 影響行数 0 = 誰かが先に更新した
  if (result.rowCount === 0) {
    throw new OptimisticLockError(table, id)
  }
  return result.rows[0]
}
// CakePHP での楽観的ロック実装
// テーブルに version (INT, DEFAULT 1) カラムを追加しておく

class OptimisticLockBehavior
{
    /**
     * version 付き更新(影響行数 0 = 競合検出)
     */
    public static function updateWithLock(
        Table $table,
        int $id,
        int $currentVersion,
        array $data
    ): EntityInterface {
        $conn = $table->getConnection();

        // UPDATE ... WHERE id = ? AND version = ?
        $affected = $conn->update(
            $table->getTable(),
            array_merge($data, [
                'version'    => $currentVersion + 1,
                'modified'   => date('Y-m-d H:i:s'),
            ]),
            ['id' => $id, 'version' => $currentVersion]
        );

        if ($affected->rowCount() === 0) {
            throw new OptimisticLockException(
                "レコード #{$id} は他のユーザーに更新されました。"
                . "画面をリロードしてください。"
            );
        }

        return $table->get($id);
    }
}

// Controller での使用
public function edit(int $id): void
{
    $order = $this->Orders->get($id);

    if ($this->request->is('put')) {
        try {
            $order = OptimisticLockBehavior::updateWithLock(
                $this->Orders,
                $id,
                (int)$this->request->getData('version'),
                $this->request->getData()
            );
            $this->Flash->success('保存しました');
        } catch (OptimisticLockException $e) {
            $this->Flash->error($e->getMessage());
        }
    }
    // フォームに hidden field で version を埋め込む
    $this->set(compact('order'));
}
// フロント: version をフォームに保持して送信
const order = ref(null) // { id, name, version, ... }

async function loadOrder(id) {
  order.value = await fetch(`/api/orders/${id}`).then(r => r.json())
}

async function saveOrder() {
  const res = await fetch(`/api/orders/${order.value.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...order.value,
      version: order.value.version, // 取得時の version を送信
    }),
  })

  if (res.status === 409) {
    // 競合検出 → ユーザーに選択肢を提示
    const latest = await res.json()
    const overwrite = confirm(
      '他のユーザーが更新しています。上書きしますか?'
    )
    if (overwrite) {
      // 最新 version で再送信
      order.value.version = latest.version
      return saveOrder() // リトライ
    } else {
      // 最新データでリフレッシュ
      order.value = latest
    }
    return
  }

  // 成功 → レスポンスの新しい version を保持
  order.value = await res.json()
  alert('保存しました')
}
🔄

リトライ + Exponential Backoff

外部連携の耐障害性を確保する
外部API、メール送信、決済処理…外部サービスは必ず失敗する。指数バックオフで適切にリトライすることで回復力を高める。
✅ こんな時に使う外部API呼び出し、ファイルアップロード、決済処理、メール送信
⚠️ 注意点バリデーションエラー(4xx)はリトライしない。一時的障害(5xx, timeout)のみ
interface RetryOptions {
  maxAttempts: number      // 最大試行回数
  baseDelayMs: number     // 初回待機時間(ms)
  maxDelayMs: number      // 最大待機時間
  shouldRetry?: (error: unknown) => boolean
}

async function withRetry<T>(
  fn: () => Promise<T>,
  opts: RetryOptions
): Promise<T> {
  let lastError: unknown

  for (let attempt = 0; attempt < opts.maxAttempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error

      // リトライ対象か判定
      if (opts.shouldRetry && !opts.shouldRetry(error)) throw error
      if (attempt === opts.maxAttempts - 1) break

      // Exponential backoff + jitter
      const delay = Math.min(
        opts.baseDelayMs * 2 ** attempt + Math.random() * 100,
        opts.maxDelayMs
      )
      await new Promise(r => setTimeout(r, delay))
    }
  }
  throw lastError
}

// 使用例:外部決済API呼び出し
const payment = await withRetry(
  () => paymentGateway.charge(amount),
  {
    maxAttempts: 3,
    baseDelayMs: 1000,  // 1s → 2s → 4s
    maxDelayMs: 10000,
    shouldRetry: (e) => e instanceof TimeoutError
  }
)
// PHP 版 Exponential Backoff + Jitter
class RetryHandler
{
    public function __construct(
        private int $maxAttempts = 3,
        private int $baseDelayMs = 1000,
        private int $maxDelayMs = 10000,
    ) {}

    /**
     * @template T
     * @param callable(): T $fn
     * @param callable(\Throwable): bool $shouldRetry
     * @return T
     */
    public function execute(callable $fn, ?callable $shouldRetry = null): mixed
    {
        $lastException = null;

        for ($attempt = 0; $attempt < $this->maxAttempts; $attempt++) {
            try {
                return $fn();
            } catch (\Throwable $e) {
                $lastException = $e;

                // リトライ対象でなければ即座に throw
                if ($shouldRetry && !$shouldRetry($e)) {
                    throw $e;
                }

                if ($attempt < $this->maxAttempts - 1) {
                    // Exponential backoff + jitter
                    $delay = min(
                        $this->baseDelayMs * (2 ** $attempt) + random_int(0, 100),
                        $this->maxDelayMs
                    );
                    usleep($delay * 1000); // ms → μs

                    Log::warning("Retry #{$attempt}: {$e->getMessage()}");
                }
            }
        }

        throw $lastException;
    }
}

// 使用例:外部決済API
$retry = new RetryHandler(maxAttempts: 3, baseDelayMs: 500);

$payment = $retry->execute(
    fn() => $gateway->charge($amount, $token),
    fn(\Throwable $e) => $e instanceof TimeoutException
        || $e instanceof ServiceUnavailableException
);
// JavaScript 版 Retry with Exponential Backoff
async function withRetry(fn, options = {}) {
  const {
    maxAttempts = 3,
    baseDelay = 1000,
    maxDelay = 10000,
    shouldRetry = () => true,
  } = options

  let lastError

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      if (!shouldRetry(error)) throw error
      if (attempt === maxAttempts - 1) break

      const delay = Math.min(
        baseDelay * 2 ** attempt + Math.random() * 100,
        maxDelay
      )
      await new Promise(r => setTimeout(r, delay))
      console.warn(`Retry #${attempt + 1}:`, error.message)
    }
  }
  throw lastError
}

// 使用例:fetch のリトライラッパー
async function fetchWithRetry(url, options = {}) {
  return withRetry(
    async () => {
      const res = await fetch(url, options)
      if (res.status >= 500) {
        throw new Error(`Server Error: ${res.status}`)
      }
      return res
    },
    {
      maxAttempts: 3,
      baseDelay: 1000,
      // 4xx はリトライしない(サーバーエラーのみ)
      shouldRetry(error) {
        return error.message.includes('Server Error')
            || error.name === 'TypeError' // ネットワーク断
      }
    }
  )
}

// Vue composable として
function useRetryFetch() {
  const retrying = ref(false)
  const attempt = ref(0)

  async function execute(url, opts) {
    retrying.value = true
    try {
      return await fetchWithRetry(url, opts)
    } finally {
      retrying.value = false
    }
  }
  return { execute, retrying, attempt }
}

📐 計算量(Big-O)早見表

O(1)
HashMap参照何万件でも一瞬
O(log n)
二分探索100万件でも20回
O(n)
線形探索 / 木探索データ量に比例
O(n log n)
ソート実用上十分高速
O(n²)
二重ループ1万件で1億回 🚨

🗺️ 業務シーン × アルゴリズム × パターン

🖥️ CRUD 画面開発

①HashMap②Sort⑦PagingRepositoryDTO

🔌 API 設計・実装

①HashMap⑤Cache⑦PagingResult型DTO

⚙️ バッチ処理

④Queue⑧TopoSort⑨SetRetryEvent

📋 管理画面

②Sort③Tree⑩State楽観ロックService

🔄 データ移行

⑨Set①HashMap⑧TopoSortResult型

💰 EC / 決済

⑩State④Queue楽観ロックRetryEvent

🚀 学習ロードマップ

Week 1–2
①HashMap → ②Sort → ⑥Regex + DTO/バリデーション
日常業務の50%をカバー
≈ 50% coverage
Week 3–4
③Tree → ⑦Paging → ⑨Set + Repository + Service Layer
管理画面・一覧系の実装力が一気に上がる
≈ 70% coverage
Week 5–6
④Queue → ⑤Cache → ⑩State + Result型 + 楽観ロック
設計レベルの判断ができるようになる
≈ 85% coverage
Week 7–8
⑧TopoSort + Event駆動 + Retry + 全体復習
アーキテクチャ議論に参加できるレベルに
≈ 100% coverage
10個のアルゴリズムを深く理解している人は、
100個を浅く知っている人に勝つ。

Web系開発者にとってアルゴリズムは、競技プログラミングのような高度な数学ではなく、「適切なデータ構造を選び、効率的にデータを操作する実践知」です。そしてパターンは、その実践知を現場で再現可能にする設計図です。