separation-of-concerns について
ソフトウェア開発者向けの関心事の分離に関するコーディングガイドです。データ取得、ビジネスロジック、表示、各層の構成を分ける方針を、兆候や例、層ごとの説明とともに示し、テスト性・再利用性・保守性の向上につなげます。
主な機能
- 01データ・ロジック・表示の分離
- 02関心事混在の兆候の特定
- 03レイヤー化されたコード例
- 04テストと再利用の利点の説明
活用シーン
- 01データ取得と表示を担うコンポーネントのリファクタリング
- 02コントローラー内の DB クエリとロジックの分離
- 03単体テストとコード再利用の改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill separation-of-concernsインストール後、Claude Code で/separation-of-concernsを入力して読み込まれたか確認します。
詳細
関心の分離
概要
各コードは一つのことだけを行うべきです。データ、ロジック、プレゼンテーションは分離する必要があります。
関心事が混在すると、テストできず、再利用も保守も難しいコードになります。分離することで、テスト、再利用、明瞭さが実現します。
使用する場面
- コンポーネントがデータの取得、変換、表示を行っている
- ビジネスロジックが UI コードに混在している
- コントローラーにデータベースクエリがある
- コードの一部を単独でテストするのが難しい
絶対的なルール
NEVER mix data fetching, business logic, and presentation in one place.
例外はありません:
- 「小さなコンポーネントだから」という理由では不可
- 「この方法のほうがシンプルだから」という理由では不可
- 「一度しか使わないから」という理由では不可
- 「動作するから」という理由では不可
検出:関心事の混在の兆候
一つのファイルで取得+変換+表示を行っている場合は、STOP:
// ❌ VIOLATION: Component does everything
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
// Business logic / transformation
const fullName = `${data.firstName} ${data.lastName}`;
const memberSince = new Date(data.createdAt).toLocaleDateString();
const isVIP = data.orderCount > 100;
setUser({ ...data, fullName, memberSince, isVIP });
setLoading(false);
});
}, [userId]);
// Presentation
if (loading) return <div>Loading...</div>;
return (
<div className="user-profile">
<h1>{user.fullName}</h1>
{user.isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {user.memberSince}</p>
</div>
);
}
問題点:
- データを取得せずにフォーマットをテストできない
- 取得ロジックを再利用できない
- プレゼンテーションを再利用できない
- コンポーネントが3つの役割を担っている
正しいパターン:レイヤーの分離
// ✅ CORRECT: Separated concerns
// 1. Data fetching (hook)
// hooks/useUser.ts
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// 2. Business logic (pure functions)
// utils/userFormatters.ts
interface FormattedUser {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function formatUser(user: User): FormattedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
memberSince: new Date(user.createdAt).toLocaleDateString(),
isVIP: user.orderCount > 100,
};
}
// 3. Presentation (dumb component)
// components/UserCard.tsx
interface UserCardProps {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function UserCard({ fullName, memberSince, isVIP }: UserCardProps) {
return (
<div className="user-profile">
<h1>{fullName}</h1>
{isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {memberSince}</p>
</div>
);
}
// 4. Composition (container component)
// pages/UserProfile.tsx
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
const formatted = formatUser(user);
return <UserCard {...formatted} />;
}
分離のメリット
| 混在 | 分離 |
|---|---|
| フォーマットをテストできない | formatUser() を単独でテストできる |
| 取得処理を再利用できない | useUser() をどこでも再利用できる |
| UI を再利用できない | UserCard をどのようなデータでも再利用できる |
| 複雑なコンポーネントが1つ | シンプルな要素が4つ |
レイヤー
1. データレイヤー(フック、サービス)
- データの取得
- API 呼び出し
- 状態管理
- ビジネスロジックは含めない
2. ロジックレイヤー(純粋関数)
- データ変換
- 計算
- 検証
- ビジネスルール
3. プレゼンテーションレイヤー(コンポーネント)
- UI のレンダリング
- スタイリング
- ユーザー操作
- データ取得は行わない
4. コンポジションレイヤー(コンテナー)
- 各レイヤーを接続する
- 読み込み状態/エラー状態を処理する
- データを下位に渡す
プレッシャーに対抗するための指針
1. 「小さなコンポーネントだから」
プレッシャー:「シンプルなケースでは、分離はやりすぎです」
**回答:**小さなものは大きくなります。最初から整理し、整理された状態を保ちましょう。
**対応:**小さなコンポーネントでも分離しましょう。コストはほとんどかかりません。
2. 「この方法のほうがシンプルだから」
プレッシャー:「すべてを一か所にまとめたほうが理解しやすいです」
**回答:**関心事が混在しているとシンプルに見えますが、テスト、デバッグ、変更が困難になります。
**対応:**長期的には、分離したほうがシンプルです。
3. 「一度しか使わないから」
プレッシャー:「このコンポーネントは固有のもので、再利用しません」
**回答:**固有のコンポーネントでも、テスト可能であることは重要です。
**対応:**再利用だけでなく、テスト可能性のためにも分離しましょう。
危険信号 - STOPして再検討する
- 取得+変換+setState を行う
useEffect - レンダー関数内のビジネスロジック
- ルートハンドラー内のデータベースクエリ
- ユーティリティ関数内の API 呼び出し
- 100行を超えるコンポーネント
これらはすべて、関心事を分離すべきサインです。
クイックリファレンス
| 関心事 | 配置場所 |
|---|---|
| API 呼び出し | フック/サービス |
| データ変換 | 純粋関数 |
| ビジネスルール | 純粋関数 |
| UI のレンダリング | プレゼンテーションコンポーネント |
| 要素の接続 | コンテナーコンポーネント |
よくある合理化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「小さなコンポーネントだから」 | 小さなものは大きくなります。今すぐ分離しましょう。 |
| 「一緒のほうがシンプル」 | 分離すれば、テストや変更がより簡単になります。 |
| 「一度しか使わないから」 | テスト可能であることは重要です。 |
| 「動作するから」 | 動作すること ≠ 保守できること。 |
| 「過剰設計だ」 | これは単なるエンジニアリングです。 |
要点
データ取得はフックに、ロジックは純粋関数に、UI はコンポーネントに配置しましょう。
分離することで、テスト、再利用、保守が可能になります。コンポーネントは、データの取得、変換、表示のいずれかを行うべきであり、3つすべてを行ってはいけません。