mvc-architecture について
mvc-architecture は、Model-View-Controller パターンでコードを構成・保守するための指針です。各レイヤーの役割、データフロー、テスト、アンチパターン、既存コードベースからの移行を扱います。
主な機能
- 01Model、View、Controller の役割
- 02ディレクトリー構成の指針
- 03レイヤー間のデータフロー
- 04各レイヤーのテスト戦略
- 05移行手順とアンチパターン
活用シーン
- 01MVC のコードベース構成の計画
- 02ビジネスロジックと UI コンポーネントの分離
- 03各レイヤーの個別テスト
- 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 に移行する場合:
- レイヤーを特定する:既存のコードを M、V、C に対応付ける
- まずモデルを抽出する:データ型とビジネスロジックを切り出す
- コントローラーを作成する:既存のデータ取得処理をフック/コントローラーでラップする
- ビューを整理する:コンポーネントからビジネスロジックを取り除く
- テストを追加する:各レイヤーを個別にテストする
まとめ
| レイヤー | 含まれるもの | 依存先 | テスト方法 |
|---|---|---|---|
| モデル | ビジネスロジック、型、サービス | なし(純粋) | 単体テスト |
| ビュー | UI コンポーネント、スタイル | Props のみ | コンポーネントテスト |
| コントローラー | フック、ハンドラー、ミドルウェア | モデル、外部 API | 統合テスト |
コメント
Markdown 対応