mcpField
M
mvc-architecture

mvc-architecture

github.com/ds-codi開発40 回インストール
リポジトリ

mvc-architecture について

mvc-architecture は、Model-View-Controller パターンでコードを構成・保守するための指針です。各レイヤーの役割、データフロー、テスト、アンチパターン、既存コードベースからの移行を扱います。

主な機能

  1. 01Model、View、Controller の役割
  2. 02ディレクトリー構成の指針
  3. 03レイヤー間のデータフロー
  4. 04各レイヤーのテスト戦略
  5. 05移行手順とアンチパターン

活用シーン

  1. 01MVC のコードベース構成の計画
  2. 02ビジネスロジックと UI コンポーネントの分離
  3. 03各レイヤーの個別テスト
  4. 04既存コードベースの MVC への移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ds-codi/project-memory-mcp --skill mvc-architecture

インストール後、Claude Code で/mvc-architectureを入力して読み込まれたか確認します。

詳細

MVC アーキテクチャのガイドライン

このドキュメントでは、コードベースで Model-View-Controller(MVC)アーキテクチャを実装し、維持するためのガイドラインを紹介します。

基本原則

1. 関心の分離
  • モデル:データ構造、ビジネスロジック、状態管理
  • ビュー:UI コンポーネント、プレゼンテーションロジック
  • コントローラー:リクエスト処理、オーケストレーション、データフロー
2. ディレクトリ構成
src/
├── models/           # Data models and business logic
│   ├── types/       # Type definitions
│   ├── entities/    # Domain entities
│   ├── services/    # Business logic services
│   └── repositories/ # Data access layer
│
├── views/            # UI components
│   ├── components/  # Reusable UI components
│   ├── pages/       # Page-level components
│   ├── layouts/     # Layout components
│   └── styles/      # Stylesheets
│
├── controllers/      # Request handlers and orchestration
│   ├── api/         # API route handlers
│   ├── hooks/       # React hooks (for React apps)
│   └── middleware/  # Request middleware
│
└── utils/            # Shared utilities
    ├── helpers/     # Helper functions
    └── constants/   # Application constants

モデル層

責務
  • データ構造と型を定義する
  • ビジネスルールとバリデーションを実装する
  • データの永続化と取得を処理する
  • アプリケーションの状態を管理する
ベストプラクティス
// models/entities/User.ts
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

// models/services/UserService.ts
export class UserService {
  constructor(private repository: UserRepository) {}

  async createUser(data: CreateUserDTO): Promise<User> {
    // Business logic validation
    if (!this.isValidEmail(data.email)) {
      throw new ValidationError('Invalid email');
    }
    return this.repository.create(data);
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

// models/repositories/UserRepository.ts
export interface UserRepository {
  create(data: CreateUserDTO): Promise<User>;
  findById(id: string): Promise<User | null>;
  update(id: string, data: UpdateUserDTO): Promise<User>;
  delete(id: string): Promise<void>;
}
ガイドライン
  • モデルは純粋に保ち、UI ロジックを含めない
  • テスト容易性のために依存性注入を使用する
  • データアクセスにはリポジトリパターンを実装する
  • すべてのサービスに明確なインターフェースを定義する

ビュー層

責務
  • UI コンポーネントをレンダリングする
  • ユーザー操作を処理する
  • モデルのデータを表示する
  • ローカルのコンポーネント状態のみを管理する
ベストプラクティス
// views/components/UserCard.tsx
interface UserCardProps {
  user: User;
  onEdit: () => void;
  onDelete: () => void;
}

export function UserCard({ user, onEdit, onDelete }: UserCardProps) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <div className="actions">
        <button onClick={onEdit}>Edit</button>
        <button onClick={onDelete}>Delete</button>
      </div>
    </div>
  );
}

// views/pages/UsersPage.tsx
export function UsersPage() {
  const { users, isLoading, error, deleteUser } = useUsers();

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div className="users-page">
      <h1>Users</h1>
      <UserList users={users} onDelete={deleteUser} />
    </div>
  );
}
ガイドライン
  • コンポーネントは小さく、役割を絞る
  • 継承よりもコンポジションを使用する
  • Props は不変にする
  • ビューにビジネスロジックを含めない
  • 適切な場合はプレゼンテーション/コンテナパターンを使用する

コントローラー層

責務
  • 受信リクエストを処理する
  • モデルとビューの間を調整する
  • データフローを管理する
  • エラーと例外的なケースを処理する
ベストプラクティス
// controllers/hooks/useUsers.ts
export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    try {
      setIsLoading(true);
      const data = await userService.getAll();
      setUsers(data);
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsLoading(false);
    }
  };

  const deleteUser = async (id: string) => {
    await userService.delete(id);
    setUsers(users.filter(u => u.id !== id));
  };

  return { users, isLoading, error, deleteUser, refetch: fetchUsers };
}

// controllers/api/usersController.ts (for backend)
export class UsersController {
  constructor(private userService: UserService) {}

  async getAll(req: Request, res: Response) {
    try {
      const users = await this.userService.getAll();
      res.json(users);
    } catch (error) {
      res.status(500).json({ error: 'Failed to fetch users' });
    }
  }

  async create(req: Request, res: Response) {
    try {
      const user = await this.userService.createUser(req.body);
      res.status(201).json(user);
    } catch (error) {
      if (error instanceof ValidationError) {
        res.status(400).json({ error: error.message });
      } else {
        res.status(500).json({ error: 'Failed to create user' });
      }
    }
  }
}
ガイドライン
  • コントローラーは薄く保ち、サービスに処理を委譲する
  • すべてのエラーケースを処理する
  • モデルに渡す前に入力を検証する
  • 横断的関心事にはミドルウェアを使用する

データフロー

User Action → View → Controller → Model → Controller → View → Updated UI
     │           │         │          │         │          │
     │           │         │          │         │          └─ Re-render
     │           │         │          │         └─ Update state
     │           │         │          └─ Business logic
     │           │         └─ Handle request
     │           └─ Event handler
     └─ Click/Input

テスト戦略

モデルのテスト
describe('UserService', () => {
  it('should validate email before creating user', async () => {
    const service = new UserService(mockRepository);
    await expect(service.createUser({ email: 'invalid' }))
      .rejects.toThrow('Invalid email');
  });
});
ビューのテスト
describe('UserCard', () => {
  it('should display user information', () => {
    render(<UserCard user={mockUser} onEdit={jest.fn()} onDelete={jest.fn()} />);
    expect(screen.getByText(mockUser.name)).toBeInTheDocument();
  });
});
コントローラーのテスト
describe('useUsers', () => {
  it('should fetch users on mount', async () => {
    const { result } = renderHook(() => useUsers());
    await waitFor(() => expect(result.current.isLoading).toBe(false));
    expect(result.current.users).toHaveLength(2);
  });
});

避けるべき一般的なアンチパターン

❌ ビューにビジネスロジックを含める
// BAD
function UserList({ users }) {
  const activeUsers = users.filter(u => u.status === 'active' && u.lastLogin > Date.now() - 86400000);
  // ...
}
✅ モデル/サービスに移す
// GOOD
function UserList({ users }) {
  const activeUsers = userService.getActiveUsers(users);
  // ...
}
❌ ビューで直接データにアクセスする
// BAD
function Dashboard() {
  const [data, setData] = useState([]);
  useEffect(() => {
    fetch('/api/data').then(r => r.json()).then(setData);
  }, []);
}
✅ コントローラー/フックを使用する
// GOOD
function Dashboard() {
  const { data, isLoading } = useDashboardData();
}

移行戦略

既存のコードベースを MVC に移行する場合:

  1. レイヤーを特定する:既存のコードを M、V、C に対応付ける
  2. まずモデルを抽出する:データ型とビジネスロジックを切り出す
  3. コントローラーを作成する:既存のデータ取得処理をフック/コントローラーでラップする
  4. ビューを整理する:コンポーネントからビジネスロジックを取り除く
  5. テストを追加する:各レイヤーを個別にテストする

まとめ

レイヤー含まれるもの依存先テスト方法
モデルビジネスロジック、型、サービスなし(純粋)単体テスト
ビューUI コンポーネント、スタイルProps のみコンポーネントテスト
コントローラーフック、ハンドラー、ミドルウェアモデル、外部 API統合テスト

コメント