mvc-architecture
4
总安装量
2
周安装量
#47820
全站排名
安装命令
npx skills add https://github.com/ds-codi/project-memory-mcp --skill mvc-architecture
Agent 安装分布
opencode
2
gemini-cli
2
antigravity
2
claude-code
2
github-copilot
2
codex
2
Skill 文档
MVC Architecture Guidelines
This document provides guidelines for implementing and maintaining a Model-View-Controller (MVC) architecture in your codebase.
Core Principles
1. Separation of Concerns
- Models: Data structures, business logic, and state management
- Views: UI components and presentation logic
- Controllers: Request handling, orchestration, and data flow
2. Directory Structure
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 Layer
Responsibilities
- Define data structures and types
- Implement business rules and validation
- Handle data persistence and retrieval
- Manage application state
Best Practices
// 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>;
}
Guidelines
- Keep models pure and free of UI logic
- Use dependency injection for testability
- Implement repository pattern for data access
- Define clear interfaces for all services
Views Layer
Responsibilities
- Render UI components
- Handle user interactions
- Display data from models
- Manage local component state only
Best Practices
// 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>
);
}
Guidelines
- Keep components small and focused
- Use composition over inheritance
- Props should be immutable
- Avoid business logic in views
- Use presentational/container pattern when appropriate
Controllers Layer
Responsibilities
- Handle incoming requests
- Coordinate between models and views
- Manage data flow
- Handle errors and edge cases
Best Practices
// 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' });
}
}
}
}
Guidelines
- Controllers should be thin – delegate to services
- Handle all error cases
- Validate input before passing to models
- Use middleware for cross-cutting concerns
Data Flow
User Action â View â Controller â Model â Controller â View â Updated UI
â â â â â â
â â â â â ââ Re-render
â â â â ââ Update state
â â â ââ Business logic
â â ââ Handle request
â ââ Event handler
ââ Click/Input
Testing Strategy
Model Tests
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');
});
});
View Tests
describe('UserCard', () => {
it('should display user information', () => {
render(<UserCard user={mockUser} onEdit={jest.fn()} onDelete={jest.fn()} />);
expect(screen.getByText(mockUser.name)).toBeInTheDocument();
});
});
Controller Tests
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);
});
});
Common Anti-Patterns to Avoid
â Business Logic in Views
// BAD
function UserList({ users }) {
const activeUsers = users.filter(u => u.status === 'active' && u.lastLogin > Date.now() - 86400000);
// ...
}
â Move to Model/Service
// GOOD
function UserList({ users }) {
const activeUsers = userService.getActiveUsers(users);
// ...
}
â Direct Data Access in Views
// BAD
function Dashboard() {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
}
â Use Controllers/Hooks
// GOOD
function Dashboard() {
const { data, isLoading } = useDashboardData();
}
Migration Strategy
If converting an existing codebase to MVC:
- Identify Layers: Map existing code to M, V, or C
- Extract Models First: Pull out data types and business logic
- Create Controllers: Wrap existing data fetching in hooks/controllers
- Clean Views: Remove business logic from components
- Add Tests: Write tests for each layer independently
Summary
| Layer | Contains | Depends On | Tested With |
|---|---|---|---|
| Model | Business logic, types, services | Nothing (pure) | Unit tests |
| View | UI components, styles | Props only | Component tests |
| Controller | Hooks, handlers, middleware | Models, external APIs | Integration tests |