758 lines
16 KiB
Markdown
758 lines
16 KiB
Markdown
# Patterns d'Architecture - ChartBastan
|
|
|
|
## Vue d'ensemble
|
|
|
|
ChartBastan utilise une architecture **client-serveur** avec:
|
|
- **Frontend:** Application React/Next.js (SPA avec SSR)
|
|
- **Backend:** API REST FastAPI
|
|
- **Communication:** REST API via HTTP
|
|
- **Architecture asynchrone:** RabbitMQ pour tâches lourdes
|
|
|
|
---
|
|
|
|
## Partie 1: Frontend Architecture (chartbastan/)
|
|
|
|
### Type d'Architecture: Component-Based avec App Router
|
|
|
|
**Framework:** Next.js 16 App Router
|
|
|
|
**Caractéristiques Principales:**
|
|
|
|
#### 1. Server Components vs Client Components
|
|
|
|
**Server Components (par défaut):**
|
|
- Exécutés sur le serveur
|
|
- Rendu HTML côté serveur
|
|
- Accès direct aux ressources serveur
|
|
- Pas d'hydratation nécessaire
|
|
- Utilisés pour: layouts, pages, data fetching initial
|
|
|
|
**Client Components (directive 'use client'):**
|
|
- Exécutés sur le client
|
|
- Hydratation côté client
|
|
- Interactivité utilisateur (events, state)
|
|
- Utilisés pour: formulaires, interactions, state local
|
|
|
|
**Exemple:**
|
|
|
|
```typescript
|
|
// Server Component (par défaut)
|
|
export default async function MatchPage() {
|
|
const matches = await fetchMatches(); // Exécuté sur serveur
|
|
return <MatchList matches={matches} />;
|
|
}
|
|
|
|
// Client Component
|
|
"use client";
|
|
export function MatchList({ matches }: { matches: Match[] }) {
|
|
const [selected, setSelected] = useState(null); // State local
|
|
return <div onClick={() => setSelected(matches[0])} />;
|
|
}
|
|
```
|
|
|
|
#### 2. App Router Structure
|
|
|
|
**Routing:** Système de fichiers dans `src/app/`
|
|
|
|
**Conventions:**
|
|
- `page.tsx` - Page principale de la route
|
|
- `layout.tsx` - Layout partagé pour la route et ses enfants
|
|
- `loading.tsx` - UI de chargement (automatic streaming)
|
|
- `error.tsx` - UI d'erreur (error boundary)
|
|
- `not-found.tsx` - UI pour 404
|
|
- `route.ts` - API routes (backend intégré)
|
|
|
|
**Exemple de structure:**
|
|
|
|
```
|
|
src/app/
|
|
├── layout.tsx # Layout racine
|
|
├── page.tsx # Page d'accueil (/)
|
|
├── login/
|
|
│ └── page.tsx # Page de login (/login)
|
|
├── matches/
|
|
│ ├── page.tsx # Liste des matchs (/matches)
|
|
│ ├── [id]/
|
|
│ │ └── page.tsx # Détail d'un match (/matches/123)
|
|
│ └── loading.tsx # Loading pour /matches
|
|
└── api/
|
|
└── auth/
|
|
└── route.ts # API route POST /api/auth
|
|
```
|
|
|
|
#### 3. Data Fetching Strategy
|
|
|
|
**Approche Hybride:**
|
|
|
|
**Server-Side Rendering (SSR):**
|
|
- Data fetch dans Server Components
|
|
- Cache avec React Cache
|
|
- Revalidation avec revalidatePath
|
|
- Avantages: SEO, premier rendu rapide
|
|
|
|
**Client-Side Fetching:**
|
|
- React Query pour data interactive
|
|
- Cache local et synchronisation
|
|
- Refetch automatique
|
|
- Optimistic updates
|
|
|
|
**Exemple:**
|
|
|
|
```typescript
|
|
// Server Component: SSR
|
|
import { fetchMatches } from '@/lib/data';
|
|
|
|
export default async function MatchesPage() {
|
|
const matches = await fetchMatches(); // Cache automatique
|
|
return <MatchList initialData={matches} />;
|
|
}
|
|
|
|
// Client Component: React Query
|
|
"use client";
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
export function MatchList({ initialData }) {
|
|
const { data } = useQuery({
|
|
queryKey: ['matches'],
|
|
queryFn: fetchMatches,
|
|
initialData, // Hydratation depuis SSR
|
|
refetchInterval: 30000, // Refetch toutes les 30s
|
|
});
|
|
}
|
|
```
|
|
|
|
#### 4. State Management
|
|
|
|
**Zustand (Global State):**
|
|
- Stores légers et simples
|
|
- Pas de Provider/Context nécessaires
|
|
- Sélecteurs pour optimisation
|
|
- DevTools intégrés
|
|
|
|
**Exemple:**
|
|
|
|
```typescript
|
|
// store/user.ts
|
|
import { create } from 'zustand';
|
|
|
|
interface UserStore {
|
|
user: User | null;
|
|
setUser: (user: User) => void;
|
|
}
|
|
|
|
export const useUserStore = create<UserStore>((set) => ({
|
|
user: null,
|
|
setUser: (user) => set({ user }),
|
|
}));
|
|
|
|
// Utilisation
|
|
export function UserProfile() {
|
|
const user = useUserStore((state) => state.user);
|
|
return <div>{user?.name}</div>;
|
|
}
|
|
```
|
|
|
|
**React Query (Server State):**
|
|
- Cache et synchronisation des données serveur
|
|
- Refetch, mutation, invalidation automatique
|
|
- Gestion optimiste
|
|
- Loading et error states
|
|
|
|
**Exemple:**
|
|
|
|
```typescript
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['matches'],
|
|
queryFn: () => fetch('/api/matches').then(r => r.json()),
|
|
staleTime: 5000, // Données fraîches pendant 5s
|
|
});
|
|
|
|
if (isLoading) return <Loading />;
|
|
if (error) return <Error message={error.message} />;
|
|
return <MatchList matches={data} />;
|
|
```
|
|
|
|
#### 5. Middleware
|
|
|
|
**Next.js Middleware:**
|
|
- Authentification (better-auth)
|
|
- Protection de routes
|
|
- Redirections
|
|
- Headers et cookies
|
|
|
|
**Exemple:**
|
|
|
|
```typescript
|
|
// middleware.ts
|
|
import { auth } from "@/lib/auth";
|
|
|
|
export default auth((req) => {
|
|
const isLoggedIn = !!req.auth;
|
|
const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");
|
|
|
|
if (!isLoggedIn && isOnDashboard) {
|
|
return Response.redirect(new URL("/login", req.url));
|
|
}
|
|
});
|
|
|
|
export const config = {
|
|
matcher: ["/dashboard/:path*", "/api/auth/:path*"],
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Architecture des Composants
|
|
|
|
#### 1. Component Hierarchy
|
|
|
|
**Composition de composants:**
|
|
- Pages → Layouts → Sections → Components → UI Elements
|
|
|
|
**Exemple:**
|
|
|
|
```
|
|
MatchPage (Page)
|
|
└── MatchLayout (Layout)
|
|
├── MatchHeader (Section)
|
|
│ └── MatchTitle (Component)
|
|
├── MatchContent (Section)
|
|
│ ├── MatchInfo (Component)
|
|
│ ├── MatchStats (Component)
|
|
│ │ └── StatCard (UI Element)
|
|
│ └── MatchPredictions (Component)
|
|
│ └── PredictionCard (UI Element)
|
|
└── MatchFooter (Section)
|
|
```
|
|
|
|
#### 2. Component Patterns
|
|
|
|
**Container/Presentational Pattern:**
|
|
|
|
```typescript
|
|
// Container: Logique et state
|
|
export function MatchContainer() {
|
|
const { data, isLoading } = useMatch(matchId);
|
|
const { predict } = usePrediction();
|
|
|
|
if (isLoading) return <Loading />;
|
|
return <MatchView match={data} onPredict={predict} />;
|
|
}
|
|
|
|
// Presentational: UI pure
|
|
export function MatchView({ match, onPredict }: Props) {
|
|
return (
|
|
<div>
|
|
<MatchHeader match={match} />
|
|
<PredictButton onClick={() => onPredict(match.id)} />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Compound Components Pattern (shadcn/ui):**
|
|
|
|
```typescript
|
|
<Dialog>
|
|
<DialogTrigger>Open</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Title</DialogTitle>
|
|
</DialogHeader>
|
|
</DialogContent>
|
|
</Dialog>
|
|
```
|
|
|
|
#### 3. Custom Hooks
|
|
|
|
**Logique réutilisable:**
|
|
|
|
```typescript
|
|
// hooks/use-matches.ts
|
|
export function useMatches() {
|
|
return useQuery({
|
|
queryKey: ['matches'],
|
|
queryFn: fetchMatches,
|
|
});
|
|
}
|
|
|
|
// hooks/use-auth.ts
|
|
export function useAuth() {
|
|
const { user } = useUserStore();
|
|
return { isAuthenticated: !!user, user };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Partie 2: Backend Architecture (backend/)
|
|
|
|
### Type d'Architecture: REST API / Layered Architecture
|
|
|
|
**Framework:** FastAPI
|
|
|
|
**Caractéristiques Principales:**
|
|
|
|
#### 1. Layered Architecture
|
|
|
|
**Couches:**
|
|
|
|
1. **API Layer** (`app/api/`)
|
|
- Endpoints REST
|
|
- Validation des requêtes
|
|
- Sérialisation des réponses
|
|
|
|
2. **Service Layer** (`app/services/`)
|
|
- Logique métier
|
|
- Orchestration des opérations
|
|
- Pas de dépendances directes aux modèles
|
|
|
|
3. **Repository/Model Layer** (`app/models/`)
|
|
- Accès aux données
|
|
- Opérations CRUD
|
|
- Abstraction de la base de données
|
|
|
|
4. **ML Layer** (`app/ml/`)
|
|
- Services de machine learning
|
|
- Analyse de sentiment
|
|
- Calcul de prédictions
|
|
|
|
5. **Scraper Layer** (`app/scrapers/`)
|
|
- Collecte de données externes
|
|
- Scraping Twitter/Reddit/RSS
|
|
- Normalisation des données
|
|
|
|
**Exemple de flux:**
|
|
|
|
```
|
|
Request: GET /api/matches/1
|
|
|
|
API Layer (matches.py)
|
|
↓ Validate request
|
|
↓ Call service
|
|
|
|
Service Layer (match_service.py)
|
|
↓ Get match from repository
|
|
↓ Calculate predictions (ML service)
|
|
↓ Calculate energy (sentiment service)
|
|
↓ Aggregate data
|
|
|
|
Repository Layer (match.py)
|
|
↓ Query database
|
|
↓ Return model
|
|
|
|
API Layer
|
|
↓ Serialize with Pydantic
|
|
↓ Return response
|
|
```
|
|
|
|
#### 2. Dependency Injection
|
|
|
|
**FastAPI Depends:**
|
|
- Injection de dépendances automatique
|
|
- Test facile avec mocks
|
|
- Gestion des cycles de vie
|
|
|
|
**Exemple:**
|
|
|
|
```python
|
|
# main.py
|
|
app = FastAPI()
|
|
|
|
# Dependency: Database session
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
# Dependency: Service
|
|
def get_match_service(db: Session = Depends(get_db)):
|
|
return MatchService(db)
|
|
|
|
# Usage in endpoint
|
|
@app.get("/matches/{match_id}")
|
|
async def get_match(
|
|
match_id: int,
|
|
service: MatchService = Depends(get_match_service)
|
|
):
|
|
return service.get_match(match_id)
|
|
```
|
|
|
|
#### 3. Pydantic Validation
|
|
|
|
**Schémas de validation:**
|
|
|
|
```python
|
|
# schemas/match.py
|
|
from pydantic import BaseModel
|
|
|
|
class MatchCreate(BaseModel):
|
|
home_team: str
|
|
away_team: str
|
|
scheduled_date: datetime
|
|
league: str
|
|
|
|
class MatchResponse(BaseModel):
|
|
id: int
|
|
home_team: str
|
|
away_team: str
|
|
scheduled_date: datetime
|
|
predictions: List[PredictionResponse]
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
```
|
|
|
|
**Utilisation:**
|
|
|
|
```python
|
|
@app.post("/matches", response_model=MatchResponse)
|
|
async def create_match(
|
|
match_data: MatchCreate,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
# Validation automatique par Pydantic
|
|
match = Match(**match_data.dict())
|
|
db.add(match)
|
|
db.commit()
|
|
return match
|
|
```
|
|
|
|
#### 4. Async/Await
|
|
|
|
**Opérations asynchrones:**
|
|
|
|
```python
|
|
# scrapers/twitter_scraper.py
|
|
async def scrape_tweets(match_id: int):
|
|
# Opération async (non-bloquante)
|
|
tweets = await twitter_client.search_tweets(...)
|
|
return tweets
|
|
|
|
# main.py
|
|
@app.post("/scrape/{match_id}")
|
|
async def scrape_match(match_id: int):
|
|
tweets = await scrape_tweets(match_id)
|
|
return {"count": len(tweets)}
|
|
```
|
|
|
|
#### 5. Background Tasks & Workers
|
|
|
|
**Background Tasks (FastAPI):**
|
|
|
|
```python
|
|
from fastapi import BackgroundTasks
|
|
|
|
@app.post("/predict/{match_id}")
|
|
async def predict_match(
|
|
match_id: int,
|
|
background_tasks: BackgroundTasks
|
|
):
|
|
# Task en arrière-plan
|
|
background_tasks.add_task(
|
|
calculate_prediction,
|
|
match_id
|
|
)
|
|
return {"message": "Prediction started"}
|
|
```
|
|
|
|
**RabbitMQ Workers (Indépendants):**
|
|
|
|
```python
|
|
# workers/scraping_worker.py
|
|
def consume_scraping_tasks():
|
|
connection = pika.BlockingConnection(...)
|
|
channel = connection.channel()
|
|
channel.queue_declare(queue='scraping')
|
|
|
|
for method_frame, properties, body in channel.consume('scraping'):
|
|
task = json.loads(body)
|
|
scrape_match(task['match_id'])
|
|
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture Intégrée (Frontend + Backend)
|
|
|
|
### Communication Pattern
|
|
|
|
**REST API Communication:**
|
|
|
|
```
|
|
Frontend Component
|
|
↓ React Query (Client)
|
|
↓ HTTP Fetch
|
|
Backend API Endpoint
|
|
↓ Pydantic Validation
|
|
↓ Service Layer
|
|
↓ Repository Layer
|
|
↓ Database (SQLAlchemy)
|
|
```
|
|
|
|
**Example Flow:**
|
|
|
|
```typescript
|
|
// Frontend: src/services/matches.ts
|
|
export function useMatches() {
|
|
return useQuery({
|
|
queryKey: ['matches'],
|
|
queryFn: () => fetch('/api/matches').then(r => r.json()),
|
|
});
|
|
}
|
|
```
|
|
|
|
```python
|
|
# Backend: app/api/matches.py
|
|
@app.get("/matches", response_model=List[MatchResponse])
|
|
async def get_matches(service: MatchService = Depends(get_match_service)):
|
|
return service.get_all_matches()
|
|
```
|
|
|
|
```python
|
|
# Backend: app/services/match_service.py
|
|
class MatchService:
|
|
def get_all_matches(self):
|
|
matches = self.db.query(Match).all()
|
|
# Add predictions, energy calculations
|
|
return matches
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture Asynchrone (RabbitMQ)
|
|
|
|
### Pattern Producer-Consumer
|
|
|
|
**Queue Architecture:**
|
|
|
|
```
|
|
Frontend Request
|
|
↓
|
|
Backend API (Producer)
|
|
↓ Publish to Queue
|
|
RabbitMQ
|
|
↓ Message
|
|
Worker (Consumer)
|
|
↓ Process Task
|
|
↓ Update Database
|
|
Database Update
|
|
↓
|
|
Frontend Polling/Websocket
|
|
```
|
|
|
|
**Example Tasks:**
|
|
|
|
1. **Scraping Task:**
|
|
- Producer: API endpoint when user requests scrape
|
|
- Consumer: Scraping worker
|
|
- Task: Scrape Twitter/Reddit for match
|
|
|
|
2. **Analysis Task:**
|
|
- Producer: Scraping worker after data collected
|
|
- Consumer: Sentiment analysis worker
|
|
- Task: Analyze sentiment of collected tweets/posts
|
|
|
|
3. **Prediction Task:**
|
|
- Producer: Analysis worker after sentiment calculated
|
|
- Consumer: Prediction worker
|
|
- Task: Calculate prediction based on energy
|
|
|
|
---
|
|
|
|
## Architecture de Base de Données
|
|
|
|
### Phase 1: SQLite
|
|
|
|
**Schema:**
|
|
- Frontend: `chartbastan.db` (Drizzle ORM)
|
|
- Backend: Même database (SQLAlchemy)
|
|
|
|
**Tables:**
|
|
- users (authentification)
|
|
- matches (données de matchs)
|
|
- predictions (prédictions utilisateurs)
|
|
- tweets (données Twitter)
|
|
- posts (données Reddit)
|
|
- energy_scores (énergie collective)
|
|
|
|
### Phase 2: PostgreSQL
|
|
|
|
**Avantages:**
|
|
- Meilleure performance pour grandes quantités de données
|
|
- Concurrency supérieure
|
|
- Support avancé des requêtes
|
|
- Scalabilité horizontale
|
|
|
|
**Migration:**
|
|
- Même schema SQLite → PostgreSQL
|
|
- Alembic pour migrations
|
|
- Drizzle pour frontend
|
|
|
|
---
|
|
|
|
## Patterns de Sécurité
|
|
|
|
### Frontend Security
|
|
|
|
**better-auth:**
|
|
- JWT tokens
|
|
- Session management
|
|
- Password hashing (bcryptjs)
|
|
- CSRF protection
|
|
|
|
**Next.js Security:**
|
|
- Middleware pour auth
|
|
- Protected routes
|
|
- Secure cookies (httpOnly)
|
|
|
|
### Backend Security
|
|
|
|
**FastAPI Security:**
|
|
- JWT authentication
|
|
- Pydantic validation
|
|
- CORS configuration
|
|
- SQL injection prevention (SQLAlchemy)
|
|
|
|
**Example:**
|
|
|
|
```python
|
|
# middleware/auth.py
|
|
from fastapi import Security, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
|
security = HTTPBearer()
|
|
|
|
async def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials = Security(security)
|
|
):
|
|
token = credentials.credentials
|
|
user = verify_token(token)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
return user
|
|
|
|
@app.get("/protected")
|
|
async def protected_route(user = Depends(get_current_user)):
|
|
return {"user": user}
|
|
```
|
|
|
|
---
|
|
|
|
## Patterns de Performance
|
|
|
|
### Frontend Performance
|
|
|
|
**Code Splitting:**
|
|
- Automatic code splitting (Next.js)
|
|
- Dynamic imports for heavy components
|
|
- Route-based splitting
|
|
|
|
```typescript
|
|
// Dynamic import
|
|
const MatchDetail = dynamic(() => import('@/components/MatchDetail'), {
|
|
loading: () => <Loading />,
|
|
ssr: false,
|
|
});
|
|
```
|
|
|
|
**Optimistic Updates:**
|
|
|
|
```typescript
|
|
const mutation = useMutation({
|
|
mutationFn: predictMatch,
|
|
onMutate: async (newPrediction) => {
|
|
// Cancel pending queries
|
|
await queryClient.cancelQueries(['predictions']);
|
|
// Optimistic update
|
|
queryClient.setQueryData(['predictions'], (old) => [...old, newPrediction]);
|
|
},
|
|
onError: (err, newPrediction, context) => {
|
|
// Rollback on error
|
|
queryClient.setQueryData(['predictions'], context.previousPredictions);
|
|
},
|
|
});
|
|
```
|
|
|
|
**Streaming & Suspense:**
|
|
|
|
```typescript
|
|
export default async function Page() {
|
|
return (
|
|
<Suspense fallback={<Loading />}>
|
|
<MatchList />
|
|
</Suspense>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Backend Performance
|
|
|
|
**Async Operations:**
|
|
|
|
```python
|
|
# Multiple async operations
|
|
tweets = await scrape_twitter(match_id)
|
|
posts = await scrape_reddit(match_id)
|
|
rss = await scrape_rss(match_id)
|
|
|
|
# Process in parallel
|
|
tweets, posts, rss = await asyncio.gather(
|
|
scrape_twitter(match_id),
|
|
scrape_reddit(match_id),
|
|
scrape_rss(match_id)
|
|
)
|
|
```
|
|
|
|
**Connection Pooling:**
|
|
|
|
```python
|
|
# SQLAlchemy
|
|
engine = create_engine(
|
|
DATABASE_URL,
|
|
pool_size=10,
|
|
max_overflow=20,
|
|
pool_pre_ping=True
|
|
)
|
|
```
|
|
|
|
**Caching:**
|
|
|
|
```python
|
|
from functools import lru_cache
|
|
|
|
@lru_cache(maxsize=128)
|
|
def get_prediction_model():
|
|
# Expensive operation, cached
|
|
return load_model()
|
|
```
|
|
|
|
---
|
|
|
|
## Résumé de l'Architecture
|
|
|
|
**Frontend (Next.js):**
|
|
- App Router (Server + Client Components)
|
|
- State: Zustand + React Query
|
|
- Data Fetching: SSR + Client-side
|
|
- UI: Tailwind + shadcn/ui
|
|
|
|
**Backend (FastAPI):**
|
|
- REST API Layered
|
|
- Service-oriented
|
|
- Async/await
|
|
- RabbitMQ workers
|
|
|
|
**Integration:**
|
|
- REST API communication
|
|
- Shared database (SQLite)
|
|
- Async processing with queues
|
|
|
|
**Architecture appropriée pour:**
|
|
- Application web moderne
|
|
- Real-time data
|
|
- Scaling horizontal
|
|
- Maintenance facile
|