Практика 12. Практическая работа 12. Слоистая архитектура
Цель
- Научиться проектировать приложение из трёх горизонтальных слоёв: Presentation, Service (Business) и Data.
- Закрепить принцип разделения ответственности: каждый слой решает только свою задачу.
- Отработать паттерн Repository как абстракцию доступа к данным.
- Понять направление зависимостей (сверху вниз) и применить инверсию зависимостей (DIP) с внедрением через конструктор.
- Различать модель предметной области и DTO, передаваемый наружу.
Краткая теория
Слоистая (layered) архитектура — простейший способ навести порядок в растущем приложении и не получить «большой ком грязи» (Big Ball of Mud), где один файл одновременно лезет в базу, проверяет правила и формирует ответ.
Три классических слоя:
Presentation ──> Service (Business) ──> Data- Presentation Layer — общение с внешним миром: принимает ввод (CLI, HTTP), вызывает сервис, переводит результат и ошибки в формат ответа (dict/JSON). Бизнес-правил не содержит.
- Service / Business Layer — сердце приложения: валидация по бизнес-правилам, сценарии использования, координация. Не знает, как пришёл запрос и где хранятся данные.
- Data Layer — доступ к хранилищу через паттерн Repository: читает и сохраняет сущности, скрывает детали хранения. Бизнес-правил не содержит.
Ключевые правила:
- Направление зависимостей строго сверху вниз. Представление знает о сервисе, сервис — об абстракции репозитория, слой данных не знает о соседях выше. Нельзя «перепрыгивать» через слой (контроллер напрямую в репозиторий) и нельзя разворачивать стрелку вверх.
- Зависимость от абстракции. Сервис зависит от интерфейса репозитория (
ABC/Protocol), а конкретную реализацию получает извне через конструктор (внедрение зависимостей, DIP). - Модель и DTO. Внутри слоёв ходят модели предметной области (
Task,Product), наружу контроллер отдаёт DTO — простой словарь без внутренних полей.
Подробности и сквозной пример — в лекции 13.
Задания
В работе проектируем одно приложение — менеджер задач (Task Manager) — и развиваем его от слоя к слою. Задания 1–3 обязательны, 4–5 углубляют тему.
Рекомендуемое дерево модулей:
task_manager/├── models.py # Task — модель предметной области├── exceptions.py # TaskValidationError, TaskNotFoundError├── data/│ ├── repository.py # TaskRepository (ABC)│ └── in_memory.py # InMemoryTaskRepository├── service/│ └── task_service.py # TaskService — бизнес-логика├── presentation/│ └── controller.py # TaskController — ввод-вывод, DTO└── main.py # сборка зависимостей и запуск сценарияЗадание 1. Data Layer — модель и репозиторий
Реализуйте нижний слой.
Требования:
- Модель
Task(@dataclass):id: int,title: str,description: str,completed: bool = False,created_at: datetime. - Абстрактный интерфейс
TaskRepository(ABC)с методами:find_by_id(task_id: int) -> Optional[Task]find_all() -> List[Task]save(task: Task) -> Task(присваиваетid, если его нет)delete(task_id: int) -> None
- Реализация
InMemoryTaskRepository— хранение вdict, автоинкрементid.
Ограничения:
- Репозиторий не содержит ни одного бизнес-правила (никаких проверок «title не пустой»).
- Слой не импортирует ничего из
service/иpresentation/.
Задание 2. Service Layer — бизнес-логика
Реализуйте TaskService, инкапсулирующий правила предметной области.
Скелет:
class TaskService: def __init__(self, repository: TaskRepository) -> None: self.repository = repository # зависим от абстракции
def create_task(self, title: str, description: str = "") -> Task: ... def complete_task(self, task_id: int) -> Task: ... def get_tasks(self, filter_completed: Optional[bool] = None) -> List[Task]: ... def delete_task(self, task_id: int) -> None: ...Требования:
create_task:titleне пустой и не длиннее 100 символов, иначеTaskValidationError.complete_task/delete_task: если задачи нет —TaskNotFoundError.get_tasks(filter_completed):None— все,True/False— фильтрация по статусу.- Сервис обращается к данным только через
TaskRepository, напрямую к хранилищу не лезет. - Сервис ничего не печатает и не формирует словарей-ответов — это работа представления.
Задание 3. Presentation Layer — контроллер и DTO
Реализуйте TaskController — границу с внешним миром.
Требования:
- Конструктор принимает
TaskService. - Приватный метод
_to_dto(task: Task) -> dictпревращает модель в словарь для отдачи наружу. - Методы возвращают
dictединого формата:handle_create_task(title, description) -> dicthandle_complete_task(task_id) -> dicthandle_list_tasks(filter_completed=None) -> dicthandle_delete_task(task_id) -> dict
- Успех:
{"success": True, "task": {...}}или{"success": True, "tasks": [...]}. - Ошибка: контроллер ловит
TaskValidationError/TaskNotFoundErrorи возвращает{"success": False, "message": str(e)}.
Ограничения:
- В контроллере нет бизнес-правил (никаких проверок длины title здесь).
- Контроллер не обращается к репозиторию напрямую — только через сервис.
В main.py соберите зависимости снизу вверх и прогоните сценарий:
repository = InMemoryTaskRepository()service = TaskService(repository)controller = TaskController(service)
print(controller.handle_create_task("Изучить ООП", "Прочитать лекцию 13"))print(controller.handle_create_task("", "пусто")) # success: Falseprint(controller.handle_complete_task(1))print(controller.handle_list_tasks(filter_completed=True))Задание 4. Заменяемость слоя данных
Покажите, что слой данных можно заменить, не трогая сервис и контроллер.
Требования:
- Добавьте вторую реализацию
FileTaskRepository(TaskRepository), сохраняющую задачи в JSON-файл (модульjson). - Реализуйте сериализацию/десериализацию
Task(учтитеdatetime). - В
main.pyпереключите приложение на файловый репозиторий, изменив только одну строку сборки. - Объясните в комментарии, почему
TaskServiceиTaskControllerпри этом не меняются.
Задание 5*. Проверка границ слоёв тестами
Подтвердите тестами, что архитектура соблюдена.
Требования:
- Напишите класс-заглушку
FakeTaskRepository(TaskRepository)(без реального хранилища). - Тест:
TaskServiceсFakeTaskRepositoryотклоняет пустойtitle(TaskValidationError) — без базы и файлов. - Тест:
complete_taskдля несуществующего id даётTaskNotFoundError. - (По желанию) тест контроллера: при бизнес-ошибке возвращается
{"success": False, ...}, а не выбрасывается исключение. - Используйте
pytestили модульunittest.
Критерии оценки
| Критерий | Вес |
|---|---|
| Задание 1: модель + абстрактный и in-memory репозиторий, слой данных без бизнес-правил | 20% |
Задание 2: TaskService с валидацией и исключениями, зависит от абстракции | 25% |
Задание 3: TaskController с DTO и обработкой ошибок, без бизнес-правил | 25% |
| Задание 4: вторая реализация репозитория, замена без правок сервиса/контроллера | 15% |
| Соблюдение направления зависимостей и структуры модулей | 10% |
| Задание 5* (тесты границ слоёв) | +5% бонус |
Снижение оценки:
- Бизнес-правило в контроллере или в репозитории — минус до 15%.
- Контроллер обращается к репозиторию, минуя сервис, — минус до 10%.
- Сервис зависит от конкретного
InMemoryTaskRepository, а не от абстракции, — минус до 10%.
Вопросы для самопроверки
- Перечислите три слоя приложения и назовите зону ответственности каждого.
- В каком направлении идут зависимости между слоями? Почему репозиторий не должен знать о контроллере?
- В каком слое должна находиться проверка «title не длиннее 100 символов»? Что сломается, если перенести её в контроллер?
- Зачем
TaskServiceзависит отTaskRepository(ABC), а не отInMemoryTaskRepositoryнапрямую? - Чем модель
Taskотличается от DTO, который контроллер возвращает наружу? Зачем их разделять? - Что такое внедрение зависимостей и где в
main.pyоно происходит? - Почему при переходе с in-memory на файловый репозиторий (задание 4) не меняются сервис и контроллер?
- Проследите путь запроса «завершить задачу по id» через все три слоя сверху вниз и обратно.
- Назовите две типичные ошибки реализации слоёв и объясните, чем они опасны.
- В каком случае три слоя были бы избыточны для задачи?