なぜ責務の分離が大切なのか
新人エンジニアが直面する「現場のコードが読みにくい」問題があると思います。
ex)
コードがごちゃごちゃして読みにくい
どこから修正すればいいんだ汗
影響調査がうまくいかん(ここを変更したことで他の機能に影響ないかな汗)
これは責務があって、ごちゃごちゃになっているのが原因です。
良かったら参考までに
そもそも責務って何?🤔
なかなかスクールとか研修で教えてくれなかったりしますよね💦
簡単に言うと、そのコードが担当すべき仕事・役割のことです。
プログラミング的に言うと、
コンポーネント/関数/クラスごとに「何を担当するか」を明確にすることです。
これがないとどのファイルが何をやっているのか?がわからなくなり、
コードがぐちゃぐちゃになって他のメンバーが読みづらくなってしまいます。汗
責務の分離とは何か
コードを整理整頓するイメージです。
つまりコードやファイルの役割分担をして、
各ファイルやコードの役割を明確にしていくことです。
カバンの整理で責務の分離を例えてみる
❌ 悪い例: ぐちゃぐちゃのカバン
責務の分離をしていないと、財布も、スマホも、ペンも、飲み物も、充電器も
全部メインポケットに放り込んでいる状態です。
これだと「どこに何をしまったか」わからなくなってしまいますよね。
電車に乗る時、「あれ定期はどこに行った?💦」って毎回カバンをひっくり返して探すハメに...
✅ 良い例: 整理整頓されたカバン
- 外ポケット → スマホ、定期(すぐ使うもの)
- メインポケット → 財布、ペンケース、PC、会社の資料、家の鍵(メインアイテム、貴重品)
- 小物入れ → イヤホン、充電器(細かいもの)
これなら必要なものがすぐ取り出せますよね🔥
コードも同じです
カバンと同じように、コードも意味・役割ごとに分けて整理すれば、
「あのコードどこだっけ?」と探す時間が減ります。
そして何より、他の人(チームメンバーや未来の自分)が見た時に、
どこに何があるか一目でわかるようになります👀
💡 AIに任せっきりだと危険
ChatGPTやClaudeCodeが生成したコードをそのまま使っていると、
どんどんコードがごちゃごちゃになっていきます。
だからこそ、人間が責任を持って整理整頓する力が必要です。
ダメな例から良い例で学ぶ
実際のコードで見ていきましょう!
ユーザー一覧を表示する画面を例に、責務の分離を実践します。
ダメな例(全部入りコンポーネント)
'use client';
import { useEffect, useState } from 'react';
export default function UsersPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
// API通信
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
// フィルタリング処理
const filteredUsers = users.filter((user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// ローディング表示
if (loading) {
return (
<div className="text-center py-12">
<div className="text-2xl">読み込み中...</div>
</div>
);
}
// エラー表示
if (error) {
return (
<div className="text-red-600 p-5">
<h2 className="text-xl font-bold">エラーが発生しました</h2>
<p>{error}</p>
</div>
);
}
// メイン表示
return (
<div className="p-5 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-5">ユーザー一覧</h1>
{/* 検索フォーム */}
<div className="mb-5">
<input
type="text"
placeholder="ユーザー名で検索..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full p-3 text-base border border-gray-300 rounded"
/>
</div>
{/* ユーザーリスト */}
<div>
{filteredUsers.length === 0 ? (
<p className="text-gray-500">ユーザーが見つかりませんでした</p>
) : (
filteredUsers.map((user) => (
<div
key={user.id}
className="border border-gray-200 rounded-lg p-4 mb-3 bg-gray-50"
>
<h3 className="text-xl font-semibold mb-2">{user.name}</h3>
<p className="text-gray-600 mb-1">📧 {user.email}</p>
<p className="text-gray-600 mb-1">🏢 {user.company.name}</p>
<p className="text-gray-600">📍 {user.address.city}</p>
</div>
))
)}
</div>
</div>
);
}
😰 何が問題なのか?
このコード、一見動くし、「別にいいんじゃない?」って思うかもしれません。
でも実際にこのコードで開発を続けると...
1. スクロール地獄になる
- 1ファイルに100行以上
- 「あのコードどこだっけ?」って毎回スクロール
- 修正箇所を探すだけで疲れる
2. 何がどこにあるか分からない
- API通信のコード
- フィルタリングのロジック
- 見た目のコンポーネント
- 全部ごちゃ混ぜ → 「どこ直せばいいの?」状態
3. 再利用できない
- 他のページでもユーザー情報を使いたい
- でもこのコードはコピペするしかない
- 同じコードがあちこちに...保守地獄の始まり💀
4. チーム開発が地獄
- Aさんが検索機能を修正中
- Bさんが表示デザインを変更中
- → 同じファイルを同時に触ってコンフリクト祭り🎆
5. テストが書けない
- 「API通信だけテストしたい」→ 無理
- 「検索ロジックだけテストしたい」→ 無理
- 全部が絡み合ってるからテスト不可能
✅ 良い例: 責務を分離したコード
さっきのコードを機能ごとに整理整頓してみましょう!
📁 ファイル構成
src/
app/
users/
page.tsx # ページ(統合のみ)
features/
user/
components/
UserCard.tsx # ユーザーカード
UserList.tsx # ユーザーリスト
UserSearchInput.tsx # 検索フォーム
api/
userApi.ts # API通信
hooks/
useUsers.ts # ユーザー取得
useUserSearch.ts # 検索ロジック
types/
user.ts # 型定義
index.ts # 公開API
shared/
components/
LoadingSpinner.tsx # ローディング
ErrorMessage.tsx # エラー表示
lib/
fetcher.ts # 共通fetch
カバンで例えると:
Bad: 全部メインポケットにぐちゃぐちゃ
Good: 小物入れ、外ポケット、メインポケットに分類
1️⃣ 共通部品を作る(shared)
全機能で使い回せる部品を作ります。
export const LoadingSpinner = () => {
return (
<div className="text-center py-12">
<div className="text-2xl">読み込み中...</div>
</div>
);
};
type Props = {
message: string;
};
export const ErrorMessage = ({ message }: Props) => {
return (
<div className="text-red-600 p-5">
<h2 className="text-xl font-bold">エラーが発生しました</h2>
<p>{message}</p>
</div>
);
};
export const fetcher = async <T,>(url: string): Promise<T> => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
💡 ポイント:
-
LoadingSpinnerとErrorMessageはどの機能でも使える - 他の機能(投稿一覧、商品一覧など)でも再利用可能
- 一度作ればずっと使える!
2️⃣ ユーザー機能を作る(features/user)
型定義
まずはデータの形を決めます。
export type User = {
id: number;
name: string;
email: string;
company: {
name: string;
};
address: {
city: string;
};
};
💡 ポイント:
- TypeScriptの型を別ファイルに分離
- 「ユーザーってこういうデータだよ」が一目瞭然
- 他のファイルでも
import type { User }で使える
API通信
外部との通信を専門に担当するファイル。
import { fetcher } from '@/shared/lib/fetcher';
import type { User } from '../types/user';
const BASE_URL = 'https://jsonplaceholder.typicode.com';
export const userApi = {
// 全ユーザー取得
getAll: () => fetcher<User[]>(`${BASE_URL}/users`),
// 特定ユーザー取得
getById: (id: number) => fetcher<User>(`${BASE_URL}/users/${id}`),
};
💡 ポイント:
- API通信はここだけ見ればOK
- 「APIのURLを変更したい」→ ここだけ修正
- 他のコンポーネントは影響を受けない
カスタムフック(ロジック)
ビジネスロジックを担当。
import { useEffect, useState } from 'react';
import { userApi } from '../api/userApi';
import type { User } from '../types/user';
export const useUsers = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadUsers = async () => {
try {
setLoading(true);
const data = await userApi.getAll();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : '不明なエラー');
} finally {
setLoading(false);
}
};
loadUsers();
}, []);
return { users, loading, error };
};
💡 ポイント:
- 「ユーザーデータを取得する」ロジックだけに集中
- 他のページでも
const { users } = useUsers()で使える - テストも書きやすい!
import { useMemo, useState } from 'react';
import type { User } from '../types/user';
export const useUserSearch = (users: User[]) => {
const [searchTerm, setSearchTerm] = useState('');
const filteredUsers = useMemo(() => {
if (!searchTerm) return users;
return users.filter((user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return {
searchTerm,
setSearchTerm,
filteredUsers,
};
};
💡 ポイント:
- 検索ロジックを分離
-
useMemoでパフォーマンス最適化 - 「検索の仕様を変えたい」→ ここだけ修正
コンポーネント(見た目)
UIだけを担当。ロジックは一切持たない。
type Props = {
value: string;
onChange: (value: string) => void;
};
export const UserSearchInput = ({ value, onChange }: Props) => {
return (
<input
type="text"
placeholder="ユーザー名で検索..."
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full p-3 text-base border border-gray-300 rounded"
/>
);
};
💡 ポイント:
- 見た目だけに集中
- propsで値を受け取るだけ
- デザイン変更したい → ここだけ触ればOK
import type { User } from '../types/user';
type Props = {
user: User;
};
export const UserCard = ({ user }: Props) => {
return (
<div className="border border-gray-200 rounded-lg p-4 mb-3 bg-gray-50">
<h3 className="text-xl font-semibold mb-2">{user.name}</h3>
<p className="text-gray-600 mb-1">📧 {user.email}</p>
<p className="text-gray-600 mb-1">🏢 {user.company.name}</p>
<p className="text-gray-600">📍 {user.address.city}</p>
</div>
);
};
💡 ポイント:
- 1ユーザー分の表示だけを担当
- 他のページでも使い回せる
- Storybookでプレビューもできる!
import type { User } from '../types/user';
import { UserCard } from './UserCard';
type Props = {
users: User[];
};
export const UserList = ({ users }: Props) => {
if (users.length === 0) {
return <p className="text-gray-500">ユーザーが見つかりませんでした</p>;
}
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
};
💡 ポイント:
- リスト表示だけを担当
-
UserCardを使って表示 - コンポーネントの組み合わせ!
公開API
外部に公開するものだけをまとめる。
export { useUsers } from './hooks/useUsers';
export { useUserSearch } from './hooks/useUserSearch';
export { UserCard } from './components/UserCard';
export { UserList } from './components/UserList';
export { UserSearchInput } from './components/UserSearchInput';
export type { User } from './types/user';
💡 ポイント:
- 「ユーザー機能で使えるもの」を一箇所にまとめる
- 他のファイルは
from '@/features/user'だけでimportできる - 内部実装を隠せる(カプセル化)
3️⃣ ページで統合
最後に、全部を組み合わせるだけ!
'use client';
import {
useUsers,
useUserSearch,
UserSearchInput,
UserList
} from '@/features/user';
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
import { ErrorMessage } from '@/shared/components/ErrorMessage';
export default function UsersPage() {
const { users, loading, error } = useUsers();
const { searchTerm, setSearchTerm, filteredUsers } = useUserSearch(users);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return (
<div className="p-5 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-5">ユーザー一覧</h1>
<div className="mb-5">
<UserSearchInput value={searchTerm} onChange={setSearchTerm} />
</div>
<UserList users={filteredUsers} />
</div>
);
}
💡 ポイント:
- ページファイルはたったの30行!
- やってることは「部品を組み合わせるだけ」
- コードが超読みやすい!
🎉 何が良くなったのか?
見比べてみたらわかると思います‼︎
| 項目 | Bad Example | Good Example |
|---|---|---|
| ファイル数 | 1ファイル | 10ファイル |
| 1ファイルの行数 | 100行超え | 10〜50行 |
| 再利用 | できない | できる |
| 修正のしやすさ | どこ直せばいいか分からない | 修正箇所が明確 |
| テスト | 書けない | 書ける |
| チーム開発 | コンフリクト祭り | スムーズ |
具体的なメリット
責務の分離をすることで以下のメリットがあります。
1. 見通しが良い
「検索機能を修正したい」
→ features/user/hooks/useUserSearch.ts を開くだけ
2. 再利用できる
// 別のページでも使える!
import { UserCard } from '@/features/user';
<UserCard user={currentUser} />
3. チーム開発がスムーズ
Aさん: UserCard.tsx を修正中
Bさん: useUsers.ts を修正中
→ お互い影響しない!コンフリクトなし!
4. テストが書ける
// useUsersだけをテスト
test('ユーザーを取得できること', async () => {
const { result } = renderHook(() => useUsers());
// ...
});
5. 拡張しやすい
新機能「投稿一覧」を追加したい
→ features/post/ を作るだけ
→ 既存コードは一切触らない!
💡 責務の分離のポイント
今回のポイントです。
| 分類 | 役割 | 配置場所 |
|---|---|---|
| 型定義 | データ構造を定義 | features/user/types/ |
| API層 | 外部通信を担当 | features/user/api/ |
| Custom Hooks | ロジックを担当 | features/user/hooks/ |
| コンポーネント | 見た目を担当 | features/user/components/ |
| 共通部品 | 全体で使うもの | shared/ |
| ページ | 全体を統合 | app/users/page.tsx |
📦 改めてカバンで例えると
悪い例
大きなカバン1つに全部詰め込む
→ 何がどこにあるか分からない
→ 毎回全部ひっくり返して探す
良い例
機能ごとのポーチ(features)
├─ ユーザー機能ポーチ
│ ├─ API通信グッズ
│ ├─ ロジックグッズ
│ └─ UI部品
└─ 投稿機能ポーチ
共通ポーチ(shared)
└─ どこでも使うもの
それぞれが自分の仕事に集中できる = 責務の分離 🎯ってことです。
参考資料
Claude Codeに添削