零弐壱蜂

[単一責任の原則] 関数は1つのことを行うべき(フロントエンド視点)

概要

単一責任の原則(SRP)は、**「クラスは変更する理由を1つだけ持つべき」**というSOLID原則の1つである。「責任」とは変更の軸を意味し、変更の影響範囲を限定することで保守性・テスト容易性・再利用性が向上する。

原則の本質

SRPの核心は「変更管理」である。「責任」とは変更の軸を意味する。あるクラスを変更する理由が複数ある場合、そのクラスは複数の責任を持つ。

違反の判断方法

  • 変更理由が複数ある
  • 複数のステークホルダーが関与する
  • クラスの目的を一言で説明できない

具体例

違反例:複数の責任を持つコンポーネント

// ❌ SRP違反:データ取得、バリデーション、UI表示を1つに集約
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  // データ取得責任
  useEffect(() => {
    fetch('/api/user/123')
      .then(res => res.json())
      .then(setUser);
  }, []);

  // バリデーション責任
  const validateEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  // フォーム送信責任
  const handleSubmit = async (data: User) => {
    if (!validateEmail(data.email)) return;
    await fetch('/api/user/123', {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  };

  // UI表示責任
  if (loading) return <div>Loading...</div>;
  return <form onSubmit={...}>{/* フォームUI */}</form>;
}

データ取得、バリデーション、API呼び出し、UI表示の4つの責任を持つため、異なる理由で変更が発生する。

改善例:責任の分離

// ✅ SRP準拠:責任ごとに分離

// hooks/useUser.ts - データ取得と状態管理のみ
function useUser(userId: string) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, [userId]);

  const updateUser = async (data: User) => {
    const res = await fetch(`/api/user/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
    setUser(await res.json());
  };

  return { user, loading, updateUser };
}

// utils/validators.ts - バリデーションのみ
export const validateEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

// components/UserForm.tsx - UI表示のみ
function UserForm({ user, onSubmit }: UserFormProps) {
  return (
    <form onSubmit={onSubmit}>
      <input name="name" defaultValue={user.name} />
      <input name="email" defaultValue={user.email} />
      <button>Save</button>
    </form>
  );
}

// 使用例
function UserProfile() {
  const { user, loading, updateUser } = useUser('123');

  if (loading) return <div>Loading...</div>;

  return <UserForm user={user} onSubmit={updateUser} />;
}

各モジュールが単一責任を持ち、変更の影響が局所化される。useUserはデータ管理、validatorsはバリデーション、UserFormはUI表示のみを担当する。

フロントエンド開発におけるSRPの利点

  • 再レンダリングの最適化: プレゼンテーショナルコンポーネントはReact.memoで最適化しやすい
  • テスト容易性: モックの準備が最小限で済み、Storybookで独立してテスト可能
  • 再利用性: 単一責任のコンポーネントは複数ヵ所で再利用できる
  • 並行作業: 責任が分離されているため、複数の開発者が衝突なく作業できる
  • 段階的リファクタリング: 一部だけを独立して改善できる

注意点とトレードオフ

  • ファイル数の増加: コンポーネント、フック、サービスなどに分離するとファイル数が増える。Feature-based Structureで管理可能
  • 過度な抽象化のリスク: 極端に小さなコンポーネントを作ると、Props Drillingを引き起こす可能性がある。粒度はプロジェクトに応じて調整すべき
  • 初期開発の遅延: 初期段階では時間がかかるが、長期的に保守コストを削減できる
  • 実用主義とのバランス: プロトタイプ開発や小規模ページでは、速度を優先することも検討すべき

まとめ

SRPは「クラスは変更する理由を1つだけ持つべき」という原則である。フロントエンド開発では、コンポーネント・フック・ユーティリティの責任を分離することで、以下が実現される。

  • 変更の局所化: デザイン、API、バリデーションなど、変更が特定箇所にのみ影響する
  • テスト容易性: モックの準備が最小限で済み、独立してテストできる
  • 再利用性: 単一責任のコンポーネントは複数ヵ所で再利用できる

実践のポイント

  • 動作するコードから始める。早期最適化は避ける
  • 変更時の痛点が見えたらリファクタリングする
  • プロトタイプや小規模ページでは、実用主義を優先する
  • 100行を超えるコンポーネント、5つ以上のuseStateがある場合は分離を検討する