לדלג לתוכן

1.3 ארכיטקטורה נקייה פתרון

תרגיל 1 - ישויות - פתרון

# entities/project.py
from dataclasses import dataclass, field
from datetime import datetime


@dataclass
class Project:
    id: int
    name: str
    description: str
    owner_id: int
    created_at: datetime
    is_archived: bool = False

    def archive(self):
        self.is_archived = True

    def can_be_modified_by(self, user_id: int) -> bool:
        return self.owner_id == user_id

תרגיל 2 - Use Case - פתרון

# use_cases/project_use_cases.py
from entities.project import Project
from datetime import datetime


class ProjectRepository:
    def save(self, project: Project) -> Project: ...
    def find_by_owner(self, owner_id: int) -> list[Project]: ...
    def find_by_id(self, project_id: int) -> Project | None: ...


class CreateProjectUseCase:
    MAX_PROJECTS_PER_USER = 10

    def __init__(self, repository: ProjectRepository):
        self.repository = repository

    def execute(self, name: str, description: str, owner_id: int) -> Project:
        if not name or not name.strip():
            raise ValueError("שם הפרויקט לא יכול להיות ריק")
        if len(name) > 100:
            raise ValueError("שם הפרויקט לא יכול לעלות על 100 תווים")

        existing_projects = self.repository.find_by_owner(owner_id)
        active_projects = [p for p in existing_projects if not p.is_archived]
        if len(active_projects) >= self.MAX_PROJECTS_PER_USER:
            raise ValueError(f"לא ניתן ליצור יותר מ-{self.MAX_PROJECTS_PER_USER} פרויקטים פעילים")

        project = Project(
            id=0,
            name=name.strip(),
            description=description,
            owner_id=owner_id,
            created_at=datetime.now()
        )
        return self.repository.save(project)

תרגיל 3 - מתאם - פתרון

# adapters/in_memory_project_repository.py
from entities.project import Project
from use_cases.project_use_cases import ProjectRepository


class InMemoryProjectRepository(ProjectRepository):
    def __init__(self):
        self._projects: dict[int, Project] = {}
        self._next_id = 1

    def save(self, project: Project) -> Project:
        if project.id == 0:
            project.id = self._next_id
            self._next_id += 1
        self._projects[project.id] = project
        return project

    def find_by_owner(self, owner_id: int) -> list[Project]:
        return [p for p in self._projects.values() if p.owner_id == owner_id]

    def find_by_id(self, project_id: int) -> Project | None:
        return self._projects.get(project_id)

תרגיל 4 - בדיקה - פתרון

import pytest
from use_cases.project_use_cases import CreateProjectUseCase
from adapters.in_memory_project_repository import InMemoryProjectRepository


def test_cannot_create_more_than_10_projects():
    repo = InMemoryProjectRepository()
    use_case = CreateProjectUseCase(repo)

    # יצירת 10 פרויקטים
    for i in range(10):
        use_case.execute(f"פרויקט {i + 1}", "תיאור", owner_id=1)

    # הניסיון ה-11 צריך להיכשל
    with pytest.raises(ValueError, match="לא ניתן ליצור"):
        use_case.execute("פרויקט 11", "תיאור", owner_id=1)


def test_can_create_project_with_valid_data():
    repo = InMemoryProjectRepository()
    use_case = CreateProjectUseCase(repo)

    project = use_case.execute("הפרויקט שלי", "תיאור הפרויקט", owner_id=1)

    assert project.id > 0
    assert project.name == "הפרויקט שלי"
    assert project.owner_id == 1
    assert not project.is_archived


def test_cannot_create_project_with_empty_name():
    repo = InMemoryProjectRepository()
    use_case = CreateProjectUseCase(repo)

    with pytest.raises(ValueError, match="ריק"):
        use_case.execute("", "תיאור", owner_id=1)

שימו לב: שלוש בדיקות רצות בלי שרת, בלי SQLite, בלי FastAPI, בלי HTTP. זו עוצמת הארכיטקטורה הנקייה.