Overview
なぜこの10個なのか?
Web系システム開発の業務を分解すると、大半は以下のパターンに帰着する。
| 業務カテゴリ | 頻度 | 対応アルゴリズム |
| データの保存・取得・表示 | ★★★★★ | ハッシュマップ、CRUD操作 |
| 一覧表示・検索・絞り込み | ★★★★★ | ソート、二分探索、フィルタリング |
| 階層データの処理 | ★★★★☆ | 木構造探索(DFS/BFS) |
| バッチ処理・非同期処理 | ★★★★☆ | キュー / スタック |
| パフォーマンス最適化 | ★★★☆☆ | キャッシュ(LRU)、メモ化 |
| 入力検証・テキスト処理 | ★★★★☆ | 正規表現、文字列操作 |
| ページネーション | ★★★★☆ | オフセット / カーソルベース |
| 依存関係・実行順序の管理 | ★★★☆☆ | トポロジカルソート |
| 重複排除・集合演算 | ★★★★☆ | Set / Map 操作 |
| 状態管理・ワークフロー | ★★★☆☆ | ステートマシン |
なぜ核心か? 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]))
)
なぜ核心か? 管理画面、検索結果、ダッシュボード…一覧表示がない業務システムは存在しない。
管理画面一覧検索結果ソート動的フィルタ
// 多条件ソート + パイプラインフィルタ
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)
なぜ核心か? カテゴリツリー、組織図、メニュー構造、コメントスレッド…「親子関係」データは至るところに存在する。
カテゴリツリーパンくず組織図コメントスレッド
// フラット配列 → ツリー構造(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
}
なぜ核心か? メール送信、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 }
}
なぜ核心か? 「遅い」→ まずキャッシュを疑え。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
}
}
なぜ核心か? 入力検証、データクレンジング、テンプレート展開、ログ解析…文字列を扱わない日はない。
電話番号郵便番号テンプレートカタカナ検証
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') // ❌ コンパイルエラー
なぜ核心か? 全件取得は絶対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 }
}
}
なぜ核心か? マイグレーション順序、タスク実行順、ビルド順序…「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
}
なぜ核心か? 「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 }))
}
}
なぜ核心か? 注文ステータス、承認フロー、画面遷移…「状態」と「遷移」を一元管理すれば複雑なロジックも明快に。
// 型レベルで不正遷移を防ぐステートマシン
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]
}
}
Business Patterns
🏭 業務システム頻出パターン
アルゴリズムが「部品」なら、以下のパターンは「設計図」。現場で繰り返し登場する構造を TypeScript で実装例を示す。
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' })
})
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) // サーバー検証エラーを反映
}
}
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
}
}
}
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
}
「注文が作成された → メール送信 + 在庫更新 + ログ記録」のように、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('保存しました')
}
外部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 }
}
Complexity
📐 計算量(Big-O)早見表
Practical Mapping
🗺️ 業務シーン × アルゴリズム × パターン
🖥️ CRUD 画面開発
①HashMap②Sort⑦PagingRepositoryDTO
🔌 API 設計・実装
①HashMap⑤Cache⑦PagingResult型DTO
⚙️ バッチ処理
④Queue⑧TopoSort⑨SetRetryEvent
📋 管理画面
②Sort③Tree⑩State楽観ロックService
🔄 データ移行
⑨Set①HashMap⑧TopoSortResult型
💰 EC / 決済
⑩State④Queue楽観ロックRetryEvent
Learning Path
🚀 学習ロードマップ
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系開発者にとってアルゴリズムは、競技プログラミングのような高度な数学ではなく、「適切なデータ構造を選び、効率的にデータを操作する実践知」です。そしてパターンは、その実践知を現場で再現可能にする設計図です。