← Назад к вопросам
Приведи пример структуры проекта когда внедряем новый функционал
1.7 Middle🔥 144 комментариев
#Архитектура и паттерны
Комментарии (4)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Структура проекта при внедрении нового функционала
При добавлении нового функционала важно соблюдать архитектуру проекта. Рассмотрим практический пример: добавление системы комментариев к вопросам.
Структура на примере функционала "Comments"
frontend/
src/
app/
questions/[id]/
page.tsx # Page component
comments/
page.tsx # Comments subpage
components/
comments/
CommentList.tsx # Список комментариев
CommentList.test.tsx
CommentItem.tsx # Один комментарий
CommentItem.test.tsx
CommentForm.tsx # Форма для нового комментария
CommentForm.test.tsx
CommentActions.tsx # Лайки, ответы
CommentActions.test.tsx
hooks/
useComments.ts # Логика получения комментариев
useComments.test.ts
useCommentMutation.ts # Логика создания/удаления
useCommentMutation.test.ts
lib/
api.ts # API функции для комментариев (экспортируются из comments/api.ts)
comments/
api.ts # API вызовы: getComments, postComment, deleteComment
types.ts # интерфейсы Comment, CommentResponse
utils.ts # вспомогательные функции форматирования
contexts/
CommentsContext.tsx # Context для共享 состояния комментариев
CommentsProvider.tsx # Provider компонент
backend/
app/
api/
v1/
comments/
router.py # Роутер
schemas.py # Pydantic модели
service.py # Бизнес-логика
dependencies.py # DI (database, auth)
domain/
comments/
models.py # ORM модель Comment
value_objects.py # CommentContent, CommentRating
exceptions.py # CommentNotFound, CommentUnauthorized
application/
comments/
use_cases.py # CreateCommentUseCase, DeleteCommentUseCase
dto.py # CreateCommentDTO
infrastructure/
persistence/
comment_repository.py # Работа с БД
presentation/
http/
routes/
comments.py # HTTP endpoints
migrations/
0010_create_comments_table.sql # Миграция БД
tests/
unit/
test_comments_service.py
test_comments_repository.py
integration/
test_comments_api.py
Пример Backend кода (Python FastAPI)
# ============= domain/comments/models.py =============
from datetime import datetime
from sqlalchemy import Column, String, Integer, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from app.domain.base import Base
class Comment(Base):
__tablename__ = 'comments'
id = Column(String, primary_key=True)
question_id = Column(String, ForeignKey('questions.id'), nullable=False)
user_id = Column(String, ForeignKey('users.id'), nullable=False)
content = Column(String(2000), nullable=False)
rating = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), nullable=False)
updated_at = Column(DateTime(timezone=True), nullable=False)
# Relationships
question = relationship('Question', back_populates='comments')
user = relationship('User', back_populates='comments')
# ============= application/comments/dto.py =============
from pydantic import BaseModel, Field
from datetime import datetime
class CreateCommentDTO(BaseModel):
question_id: str = Field(..., description='Question ID')
content: str = Field(..., min_length=1, max_length=2000)
class CommentResponseDTO(BaseModel):
id: str
question_id: str
user_id: str
content: str
rating: int
created_at: datetime
updated_at: datetime
# ============= application/comments/use_cases.py =============
from app.domain.comments.models import Comment
from app.application.comments.dto import CreateCommentDTO
from datetime import datetime
from zoneinfo import ZoneInfo
class CreateCommentUseCase:
def __init__(self, comment_repo, user_service):
self.comment_repo = comment_repo
self.user_service = user_service
async def execute(self, dto: CreateCommentDTO, user_id: str) -> Comment:
# Валидация
user = await self.user_service.get_user(user_id)
if not user:
raise UserNotFoundError()
# Создание
now = datetime.now(ZoneInfo('UTC'))
comment = Comment(
id=generate_id(),
question_id=dto.question_id,
user_id=user_id,
content=dto.content,
rating=0,
created_at=now,
updated_at=now
)
# Сохранение
return await self.comment_repo.save(comment)
# ============= infrastructure/persistence/comment_repository.py =============
from sqlalchemy import select
from app.domain.comments.models import Comment
class CommentRepository:
def __init__(self, db_session):
self.db = db_session
async def save(self, comment: Comment) -> Comment:
self.db.add(comment)
await self.db.flush()
return comment
async def find_by_question(self, question_id: str) -> list[Comment]:
query = select(Comment).where(
Comment.question_id == question_id
).order_by(Comment.created_at.desc())
result = await self.db.execute(query)
return result.scalars().all()
async def delete(self, comment_id: str) -> None:
comment = await self.db.get(Comment, comment_id)
if comment:
await self.db.delete(comment)
await self.db.commit()
# ============= api/v1/comments/router.py =============
from fastapi import APIRouter, Depends, HTTPException
from app.application.comments.use_cases import CreateCommentUseCase
from app.application.comments.dto import CreateCommentDTO, CommentResponseDTO
from app.api.v1.dependencies import get_current_user
router = APIRouter(prefix='/api/v1/comments', tags=['comments'])
@router.post('')
async def create_comment(
dto: CreateCommentDTO,
user_id: str = Depends(get_current_user),
use_case: CreateCommentUseCase = Depends()
) -> CommentResponseDTO:
comment = await use_case.execute(dto, user_id)
return CommentResponseDTO.from_orm(comment)
@router.get('/questions/{question_id}')
async def get_comments(
question_id: str,
repo: CommentRepository = Depends()
) -> list[CommentResponseDTO]:
comments = await repo.find_by_question(question_id)
return [CommentResponseDTO.from_orm(c) for c in comments]
Пример Frontend кода (React/Next.js)
// ============= lib/comments/types.ts =============
export interface Comment {
id: string;
questionId: string;
userId: string;
content: string;
rating: number;
createdAt: string;
updatedAt: string;
}
export interface CreateCommentPayload {
questionId: string;
content: string;
}
// ============= lib/comments/api.ts =============
import { api } from '@/lib/api';
import { Comment, CreateCommentPayload } from './types';
export const commentsApi = {
async getComments(questionId: string): Promise<Comment[]> {
return api.get(`/api/v1/comments/questions/${questionId}`);
},
async createComment(payload: CreateCommentPayload): Promise<Comment> {
return api.post('/api/v1/comments', payload);
},
async deleteComment(commentId: string): Promise<void> {
return api.delete(`/api/v1/comments/${commentId}`);
},
async likeComment(commentId: string): Promise<void> {
return api.post(`/api/v1/comments/${commentId}/like`);
}
};
// ============= hooks/useComments.ts =============
import { useQuery } from '@tanstack/react-query';
import { commentsApi } from '@/lib/comments/api';
import { Comment } from '@/lib/comments/types';
export function useComments(questionId: string) {
return useQuery({
queryKey: ['comments', questionId],
queryFn: () => commentsApi.getComments(questionId),
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// ============= hooks/useCommentMutation.ts =============
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { commentsApi } from '@/lib/comments/api';
import { CreateCommentPayload } from '@/lib/comments/types';
export function useCreateComment(questionId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: CreateCommentPayload) =>
commentsApi.createComment(payload),
onSuccess: () => {
// Инвалидируем кеш после успешного создания
queryClient.invalidateQueries({
queryKey: ['comments', questionId]
});
},
});
}
// ============= components/comments/CommentForm.tsx =============
import { useState } from 'react';
import { useCreateComment } from '@/hooks/useCommentMutation';
import { Button } from '@/components/ui/Button';
import { Textarea } from '@/components/ui/Textarea';
interface CommentFormProps {
questionId: string;
onSuccess?: () => void;
}
export function CommentForm({ questionId, onSuccess }: CommentFormProps) {
const [content, setContent] = useState('');
const mutation = useCreateComment(questionId);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await mutation.mutateAsync({
questionId,
content,
});
setContent('');
onSuccess?.();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Your comment..."
maxLength={2000}
/>
<Button
type="submit"
disabled={mutation.isPending || !content.trim()}
>
{mutation.isPending ? 'Posting...' : 'Post Comment'}
</Button>
</form>
);
}
// ============= components/comments/CommentList.tsx =============
import { useComments } from '@/hooks/useComments';
import { CommentItem } from './CommentItem';
import { Skeleton } from '@/components/ui/Skeleton';
interface CommentListProps {
questionId: string;
}
export function CommentList({ questionId }: CommentListProps) {
const { data: comments, isLoading, error } = useComments(questionId);
if (isLoading) return <CommentListSkeleton />;
if (error) return <div>Error loading comments</div>;
if (!comments?.length) return <div>No comments yet</div>;
return (
<div className="space-y-4">
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</div>
);
}
// ============= app/questions/[id]/page.tsx =============
import { CommentList } from '@/components/comments/CommentList';
import { CommentForm } from '@/components/comments/CommentForm';
export default function QuestionPage({ params }: { params: { id: string } }) {
return (
<div className="container">
<h1>Question</h1>
<section className="mt-8">
<h2>Comments</h2>
<CommentForm questionId={params.id} />
<CommentList questionId={params.id} />
</section>
</div>
);
}
Миграция БД
-- ============= migrations/0010_create_comments_table.sql =============
-- +goose Up
CREATE TABLE comments (
id VARCHAR(36) PRIMARY KEY,
question_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
content VARCHAR(2000) NOT NULL,
rating INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_comments_question_id ON comments(question_id);
CREATE INDEX idx_comments_user_id ON comments(user_id);
CREATE INDEX idx_comments_created_at ON comments(created_at DESC);
-- +goose Down
DROP INDEX IF EXISTS idx_comments_created_at;
DROP INDEX IF EXISTS idx_comments_user_id;
DROP INDEX IF EXISTS idx_comments_question_id;
DROP TABLE IF EXISTS comments;
Тесты
// ============= components/comments/CommentForm.test.tsx =============
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CommentForm } from './CommentForm';
import { useCreateComment } from '@/hooks/useCommentMutation';
jest.mock('@/hooks/useCommentMutation');
describe('CommentForm', () => {
it('should submit comment', async () => {
const mockMutate = jest.fn();
(useCreateComment as jest.Mock).mockReturnValue({
mutateAsync: mockMutate,
isPending: false,
});
render(<CommentForm questionId="123" />);
const textarea = screen.getByPlaceholderText('Your comment...');
fireEvent.change(textarea, { target: { value: 'Great question!' } });
const button = screen.getByRole('button');
fireEvent.click(button);
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({
questionId: '123',
content: 'Great question!',
});
});
});
});
Ключевые принципы структуры
- Разделение ответственности - каждый слой имеет свои задачи
- DDD подход - domain models, use cases, services
- Clean Architecture - зависимости идут внутрь
- Тестируемость - каждый модуль тестируется отдельно
- Масштабируемость - легко добавить новые функции
Такая структура позволяет легко добавлять новые функции, тестировать код и поддерживать проект.