← Назад к вопросам

Приведи пример структуры проекта когда внедряем новый функционал

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 - зависимости идут внутрь
  • Тестируемость - каждый модуль тестируется отдельно
  • Масштабируемость - легко добавить новые функции

Такая структура позволяет легко добавлять новые функции, тестировать код и поддерживать проект.

Приведи пример структуры проекта когда внедряем новый функционал | PrepBro