🧹第11章 不要になったら消す(重複の除去)
重複は設計の悪臭です。この章では、TODOアプリにおける重複コードを発見し、 段階的に除去していきます。クリーンで保守性の高いコードを目指します。
「2つのサブクラス`Dollar`と`Franc`にはもうコンストラクタしか残っていない。 コンストラクタだけのサブクラスを残しておく理由はないので、サブクラスを消してしまいたい。」
TODOリスト
- • 重複コードの発見と除去
- •
`IncompleteTodoItem`と`CompletedTodoItem`の統合
- •
React Componentの共通化
- •
不要なテストの削除
🔍重複の発見
現在のTODOアプリには、以下の重複が存在しています:
クラスの重複
`IncompleteTodoItem`と`CompletedTodoItem`の類似実装
UIの重複
React コンポーネント間での重複ロジック
テストの重複
重複するテストケース
🔬`TodoItem`サブクラスの重複分析
現在の実装を確認してみましょう:
// IncompleteTodoItem と CompletedTodoItem の重複部分
class IncompleteTodoItem extends TodoItem {
constructor(id: number, text: string, category: TodoCategory) {
super(id, text, false, category); // completed は false
}
complete(): ITodoItem {
return TodoItem.createCompleted(this.id, this.text, this.category);
}
}
class CompletedTodoItem extends TodoItem {
constructor(id: number, text: string, category: TodoCategory) {
super(id, text, true, category); // completed は true
}
complete(): ITodoItem {
return this; // 既に完了済み
}
}
分析結果:2つのクラスの違いは`completed`フィールドの値と`complete()`メソッドの実装だけです。
🔴1. レッド:統合されたTodoItemのテストを書く
まず、サブクラスを使わない統一されたテストを書きます。
describe('統一されたTodoItem実装のテスト', () => {
it('特定の完了状態でTODOを作成すること', () => {
const incompleteTodo = TodoItem.create(1, 'TDDを学ぶ', 'LEARNING', false);
const completedTodo = TodoItem.create(2, 'テストを書く', 'WORK', true);
expect(incompleteTodo.completed).toBe(false);
expect(completedTodo.completed).toBe(true);
});
it('完了ロジックを統一的に処理すること', () => {
const todo = TodoItem.create(1, 'タスク', 'WORK', false);
const completedTodo = todo.complete();
expect(completedTodo.completed).toBe(true);
expect(completedTodo.id).toBe(todo.id);
expect(completedTodo.text).toBe(todo.text);
expect(completedTodo.category).toBe(todo.category);
});
it('既に完了済みの場合は完了状態を維持すること', () => {
const completedTodo = TodoItem.create(1, '完了済み', 'WORK', true);
const stillCompleted = completedTodo.complete();
expect(stillCompleted).toBe(completedTodo); // 同じインスタンス
});
});
🟢2. グリーン:統一されたTodoItem実装
サブクラスを削除し、単一のクラスで実装します。
type TodoCategory = 'WORK' | 'PERSONAL' | 'SHOPPING' | 'LEARNING';
export class TodoItem implements ITodoItem {
constructor(
public readonly id: number,
public readonly text: string,
public readonly completed: boolean,
public readonly category: TodoCategory
) {}
complete(): ITodoItem {
if (this.completed) {
return this; // 既に完了済み
}
return new TodoItem(this.id, this.text, true, this.category);
}
multiply(count: number): ITodoItem[] {
if (count <= 0) return [];
const result: ITodoItem[] = [];
for (let i = 0; i < count; i++) {
result.push(new TodoItem(
this.id + i,
this.text,
this.completed, // 完了状態を保持
this.category
));
}
return result;
}
equals(other: ITodoItem): boolean {
return this.id === other.id &&
this.text === other.text &&
this.completed === other.completed &&
this.category === other.category;
}
// 統一されたFactory Methods
static create(
id: number,
text: string,
category: TodoCategory = 'PERSONAL',
completed: boolean = false
): ITodoItem {
return new TodoItem(id, text, completed, category);
}
// カテゴリー別Factory Methods(簡潔化)
static work(id: number, text: string, completed = false): ITodoItem {
return TodoItem.create(id, text, 'WORK', completed);
}
static personal(id: number, text: string, completed = false): ITodoItem {
return TodoItem.create(id, text, 'PERSONAL', completed);
}
static shopping(id: number, text: string, completed = false): ITodoItem {
return TodoItem.create(id, text, 'SHOPPING', completed);
}
static learning(id: number, text: string, completed = false): ITodoItem {
return TodoItem.create(id, text, 'LEARNING', completed);
}
}
// サブクラスは削除済み ✅
🔵3. 重複するテストの削除
サブクラスの削除により、いくつかのテストが冗長になりました。
削除対象のテスト
// 削除: サブクラス固有のテスト
describe('IncompleteTodoItem specific behavior', () => {
// このテストブロック全体を削除
});
describe('CompletedTodoItem specific behavior', () => {
// このテストブロック全体を削除
});
残すべきテスト
describe('TodoItem', () => {
it('デフォルトで未完了のTODOを作成すること', () => {
const todo = TodoItem.work(1, 'コードレビュー');
expect(todo.completed).toBe(false);
});
it('complete()呼び出し時にTODOが完了すること', () => {
const todo = TodoItem.personal(1, '食材を買う');
const completed = todo.complete();
expect(completed.completed).toBe(true);
expect(completed.id).toBe(todo.id);
});
});
⚛️4. React Component の重複除去
現在、複数のコンポーネントで重複ロジックがあります。共通化を進めます。
重複の発見
`TodoItemComponent.tsx`と`TodoListComponent.tsx`でカテゴリースタイルが重複
"use client";
import React from 'react';
type TodoCategory = 'WORK' | 'PERSONAL' | 'SHOPPING' | 'LEARNING';
interface CategoryBadgeProps {
category: TodoCategory;
className?: string;
}
const categoryStyles: Record<TodoCategory, string> = {
WORK: 'bg-red-100 text-red-800',
PERSONAL: 'bg-green-100 text-green-800',
SHOPPING: 'bg-yellow-100 text-yellow-800',
LEARNING: 'bg-blue-100 text-blue-800'
};
export function CategoryBadge({ category, className = '' }: CategoryBadgeProps) {
return (
<span className={`px-2 py-1 text-xs rounded-full ${categoryStyles[category]} ${className}`}>
{category}
</span>
);
}
import { ITodoItem } from '@/lib/TodoItem';
import { CategoryBadge } from './CategoryBadge';
export function TodoItemComponent({ todo, onToggle }) {
return (
<li className="flex items-center space-x-3 p-3 border rounded-lg">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="w-4 h-4 text-blue-600"
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</span>
<CategoryBadge category={todo.category} />
</li>
);
}
🪝5. カスタムフックでの状態管理重複除去
"use client";
import { useState, useCallback } from 'react';
import { ITodoItem, TodoItem } from '@/lib/TodoItem';
export function useTodoList(initialTodos: ITodoItem[] = []) {
const [todos, setTodos] = useState<ITodoItem[]>(initialTodos);
const toggleTodo = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? todo.complete() : todo
)
);
}, []);
const addTodo = useCallback((text: string, category: string = 'PERSONAL') => {
const newId = Math.max(0, ...todos.map(t => t.id)) + 1;
const newTodo = TodoItem.create(newId, text, category as any);
setTodos(prev => [...prev, newTodo]);
}, [todos]);
const removeTodo = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return {
todos,
toggleTodo,
addTodo,
removeTodo
};
}
🏭6. 不要なFactory Methodの削除
一部のFactory Methodが重複していることがわかりました。最適化を行います。
削除前(重複あり)
// 重複するメソッド
static create(...) { ... }
static createCompleted(...) { ... } // ← 削除対象
static work(...) { ... }
static personal(...) { ... }
削除後(最適化)
// 基本Factory Method
static create(
id: number,
text: string,
category: TodoCategory = 'PERSONAL',
completed: boolean = false
): ITodoItem {
return new TodoItem(id, text, completed, category);
}
// カテゴリー別Factory Methods (createのwrapper)
static work(id: number, text: string, completed = false): ITodoItem {
return TodoItem.create(id, text, 'WORK', completed);
}
// createCompletedは削除 - createで代用可能
🚀7. Next.js App Router でのクリーンな実装
"use client";
import React from 'react';
import { useTodoList } from '@/hooks/useTodoList';
import { TodoItem } from '@/lib/TodoItem';
import { TodoItemComponent } from '@/components/TodoItemComponent';
const initialTodos = [
TodoItem.work(1, 'コードレビュー'),
TodoItem.learning(2, 'TDD設計パターン習得'),
TodoItem.personal(3, '健康管理', true), // 完了済み
];
export default function CleanTodosPage() {
const { todos, toggleTodo, addTodo } = useTodoList(initialTodos);
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Clean TODO List</h1>
<p className="text-gray-600 mb-6">
重複を除去したクリーンな実装
</p>
<ul className="space-y-2">
{todos.map((todo) => (
<TodoItemComponent
key={todo.id}
todo={todo}
onToggle={toggleTodo}
/>
))}
</ul>
<button
onClick={() => addTodo('新しいタスク', 'WORK')}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
作業タスクを追加
</button>
</main>
);
}
✨重複除去の成果
この章で除去した重複:
クラス構造
2つのサブクラスを統一クラスに統合
テストコード
冗長なテストケースを削除
UI コンポーネント
共通スタイルを`CategoryBadge`に抽出
状態管理
カスタムフックで重複ロジックを統合
Factory Methods
重複するメソッドを整理
TODOリストの更新
- • ✅ 重複コードの発見と除去
- • ✅
`IncompleteTodoItem`と`CompletedTodoItem`の統合
- • ✅
React Componentの共通化
- • ✅
不要なテストの削除
🎯まとめ
重複コードの発見と除去を通じて、より保守しやすいクリーンなコードを実現しました。 サブクラスの統合、テストの整理、UI コンポーネントの共通化により、 システム全体の複雑性が大幅に削減されました。 次の章では、「設計とメタファー」を学び、 TODOリストの概念設計とドメインモデルの構築を通じて、 より洗練された設計手法を身につけます。