3.2 ארכיטקטורת שכבות הרצאה
ארכיטקטורת שכבות - Layered Architecture¶
ארכיטקטורת שכבות היא הארכיטקטורה הנפוצה ביותר לאפליקציות server-side. הרעיון פשוט: מחלקים את הקוד לשכבות, כאשר כל שכבה אחראית על תחום אחד, ושכבה יכולה לדבר רק עם השכבה שמתחתיה.
שלוש השכבות הקלאסיות¶
+---------------------------+
| Presentation Layer | שכבת ממשק המשתמש (Routes, HTTP)
+---------------------------+
|
v
+---------------------------+
| Business Logic Layer | שכבת הלוגיקה העסקית (Services, Use Cases)
+---------------------------+
|
v
+---------------------------+
| Data Access Layer | שכבת גישה לנתונים (Repositories, DB)
+---------------------------+
כלל בסיסי: שכבה עליונה תלויה בשכבה שמתחתיה. לא הפוך.
שכבה 1: Presentation Layer - שכבת ממשק¶
שכבה זו מטפלת בכל דבר הקשור ל-HTTP:
- מקבלת בקשות HTTP
- מבצעת וולידציה בסיסית של הקלט
- מעבירה לשכבת הלוגיקה
- מחזירה תשובת HTTP
# routes/tasks.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services.task_service import TaskService
router = APIRouter(prefix="/tasks")
task_service = TaskService()
class CreateTaskRequest(BaseModel):
title: str
description: str
owner_id: int
class TaskResponse(BaseModel):
id: int
title: str
status: str
@router.post("/", response_model=TaskResponse, status_code=201)
def create_task(request: CreateTaskRequest):
try:
task = task_service.create_task(
request.title,
request.description,
request.owner_id
)
return TaskResponse(id=task["id"], title=task["title"], status=task["status"])
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{task_id}", response_model=TaskResponse)
def get_task(task_id: int):
task = task_service.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="משימה לא נמצאה")
return TaskResponse(**task)
@router.put("/{task_id}/complete")
def complete_task(task_id: int, user_id: int):
try:
task_service.complete_task(task_id, user_id)
return {"message": "משימה הושלמה"}
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
מה שכבת ה-presentation לא עושה: לוגיקה עסקית, גישה ישירה למסד הנתונים, שליחת מיילים.
שכבה 2: Business Logic Layer - שכבת הלוגיקה¶
שכבה זו היא "המוח" של האפליקציה:
- מממשת את כללי הביזנס
- מתאמת בין repositories שונים
- שולחת notifications
- לא יודעת שום דבר על HTTP
# services/task_service.py
from repositories.task_repository import TaskRepository
from repositories.user_repository import UserRepository
from services.notification_service import NotificationService
class TaskService:
def __init__(self):
self._task_repo = TaskRepository()
self._user_repo = UserRepository()
self._notifier = NotificationService()
def create_task(self, title: str, description: str, owner_id: int) -> dict:
# לוגיקה עסקית: וולידציה
if len(title.strip()) < 3:
raise ValueError("כותרת המשימה חייבת להכיל לפחות 3 תווים")
# לוגיקה עסקית: בדיקת הרשאות
user = self._user_repo.find_by_id(owner_id)
if not user:
raise ValueError(f"משתמש {owner_id} לא נמצא")
# יצירת המשימה
task = self._task_repo.save({
"title": title.strip(),
"description": description,
"status": "todo",
"owner_id": owner_id
})
# שליחת התראה
self._notifier.notify_task_created(task, user["email"])
return task
def get_task(self, task_id: int) -> dict | None:
return self._task_repo.find_by_id(task_id)
def complete_task(self, task_id: int, requesting_user_id: int):
task = self._task_repo.find_by_id(task_id)
if not task:
raise ValueError(f"משימה {task_id} לא נמצאה")
# לוגיקה עסקית: הרשאה
if task["owner_id"] != requesting_user_id:
raise PermissionError("רק הבעלים יכול להשלים את המשימה")
self._task_repo.update(task_id, status="done")
שכבה 3: Data Access Layer - שכבת הנתונים¶
שכבה זו מדברת עם מסד הנתונים בלבד:
- שמירה, שליפה, עדכון, מחיקה
- ממירה בין פורמט DB לפורמט Python
- לא יודעת על לוגיקה עסקית
# repositories/task_repository.py
import sqlite3
from datetime import datetime
class TaskRepository:
def __init__(self, db_path: str = "taskflow.db"):
self._db_path = db_path
def _get_db(self):
return sqlite3.connect(self._db_path)
def save(self, task: dict) -> dict:
db = self._get_db()
cursor = db.execute(
"INSERT INTO tasks (title, description, status, owner_id, created_at) VALUES (?, ?, ?, ?, ?)",
(task["title"], task["description"], task["status"],
task["owner_id"], datetime.now().isoformat())
)
db.commit()
return {**task, "id": cursor.lastrowid}
def find_by_id(self, task_id: int) -> dict | None:
db = self._get_db()
row = db.execute(
"SELECT id, title, description, status, owner_id FROM tasks WHERE id=?",
(task_id,)
).fetchone()
if not row:
return None
return {
"id": row[0], "title": row[1], "description": row[2],
"status": row[3], "owner_id": row[4]
}
def find_by_owner(self, owner_id: int) -> list[dict]:
db = self._get_db()
rows = db.execute(
"SELECT id, title, description, status, owner_id FROM tasks WHERE owner_id=?",
(owner_id,)
).fetchall()
return [
{"id": r[0], "title": r[1], "description": r[2], "status": r[3], "owner_id": r[4]}
for r in rows
]
def update(self, task_id: int, **fields):
if not fields:
return
set_clause = ", ".join(f"{k}=?" for k in fields)
values = list(fields.values()) + [task_id]
db = self._get_db()
db.execute(f"UPDATE tasks SET {set_clause} WHERE id=?", values)
db.commit()
מבנה הקבצים¶
taskflow/
main.py
routes/
tasks.py
users.py
projects.py
services/
task_service.py
user_service.py
notification_service.py
repositories/
task_repository.py
user_repository.py
project_repository.py
models/
task.py (Pydantic schemas)
user.py
היתרונות של ארכיטקטורת שכבות¶
testability: אפשר לבדוק כל שכבה בנפרד. service tests עם mock repositories.
maintainability: רוצים לעבור מ-SQLite ל-PostgreSQL? משנים רק את שכבת ה-repositories.
readability: מפתח חדש יודע לאן ללכת - לוגיקה ב-service, DB ב-repository.
separation of concerns: כל שכבה יודעת רק מה שהיא צריכה לדעת.
מלכודת נפוצה: Anemic Domain Model¶
שכבות לא אומרות שה-entity הוא רק מבנה נתונים ריק. לוגיקה שמתייחסת לentity צריכה להיות בו:
# לא טוב - Task ריק מלוגיקה
class Task:
def __init__(self, id, title, status): ...
class TaskService:
def is_overdue(self, task): ... # לוגיקה של Task נמצאת בservice?
# טוב - Task מכיל לוגיקה שקשורה לו
class Task:
def is_overdue(self) -> bool:
return self.due_date and datetime.now() > self.due_date and self.status != "done"