\input{preambule.tex} \begin{document} \thispagestyle{empty} \centerline{МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ} \centerline{НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ НИЖЕГОРОДСКИЙ} \centerline{ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ИМ Н. И. ЛОБАЧЕВСКОГО} \centerline{Радиофизический факультет} \vfill \centerline{\Large{Отчет к лабораторной работе}} \centerline{\large{по Методам программирования}} \centerline{\Large{Поиск выхода из лабиринта }} \centerline{\Large{(объектно-ориентированная реализация с паттернами)}} \vfill Студент группы 427 \hfill Пронин Владислав Владимирович Преподаватель \hfill Морозов Н. С. \vfill \centerline{Н. Новгород, 2026} \clearpage \newpage \tableofcontents \newpage \section{Цель работы} Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. \section{Описание задачи и выбранных паттернов} Используемые Паттерны: \begin{itemize} \item Strategy (Стратегия) \textemdash \ это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы. Выбран, так как в данной лабораторной работе используются несколько алгоритмов, выполняющих одно и то же действие \ \textemdash \ обход графа. \item Builder (строитель) \textemdash \ абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Позволяет отделить построение сложного объекта от его представления, создает сложные объекты, используя простые объекты и поэтапный подход. Выбран для изоляции сложного процесса парсинга текстовоо файла. \item Observer (Наблюдатель) \ \textemdash \ это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. Выбран для отделения логики приложения от вывода на экран (принцип MVC). Класс ConsoleView подписывается на события GameController и перерисовывает карту только тогда, когда игрок перемещается или путь найден. \end{itemize} \section{Диаграмма классов} \begin{figure}[H] \centering \includegraphics[scale=0.06]{plan.png} \end{figure} \section{Листиги Классов} \subsection{Maze Solver} \begin{lstlisting} import time from Maze import Maze from strategy import PathFindingStrategy class MazeSolver: def __init__(self, maze: Maze, strategy: PathFindingStrategy): self._maze = maze self._strategy = strategy self._observers = [] def addObserver(self, observer): """Регистрация нового наблюдателя (например, ConsoleView)""" self._observers.append(observer) def notify(self, event): """Уведомление всех подписчиков о событии""" for observer in self._observers: observer.update(event) def setStrategy(self, strategy): self._strategy = strategy def solve(self): if not self._maze or not self._strategy: raise ValueError("Не задан лабиринт или стратегия поиска!") start_time = time.perf_counter() path, visited_count = self._strategy.findPath( self._maze, self._maze.start, self._maze.exit) end_time = time.perf_counter() execution_time_ms = (end_time - start_time) * 1000 path_length = len(path) from ConsoleView import Event self.notify(Event("path_found", {"maze": self._maze, "path": path})) return SearchStats(execution_time_ms, visited_count, path_length, path) \end{lstlisting} \subsection{Maze Builder} \begin{lstlisting} from abc import ABC, abstractmethod from Maze import Maze, Cell class MazeBuilder(ABC): @abstractmethod def buildFromFile(self, filename): pass class TextFileMazeBuilder(MazeBuilder): def __init__(self): self._maze = None @property def maze(self): return self._maze def buildFromFile(self, filename: str): with open(filename, mode='r', encoding='utf-8') as file: lines = file.read().splitlines() height = len(lines) width = len(lines[0]) self._maze = Maze(height, width) for y, line in enumerate(lines): for x, char in enumerate(line): cell = self._maze.getCell(x, y) if char == '#': cell.isWall = True elif char == 'S': cell.isStart = True self._maze.start = cell elif char == 'E': cell.isExit = True self._maze.exit = cell self._validate() return self._maze def _validate(self): if self._maze.start is None: raise "в лабиринте нет старта" if self._maze.exit is None: raise "в лабиринте нет начала" \end{lstlisting} \subsection{OBserver} \begin{lstlisting} from Observer import Observer, Event class ConsoleView(Observer): def update(self, event: Event) -> None: if event.type == "maze_loaded": print("\n[Система] Лабиринт успешно загружен!") self.render(event.data.get("maze")) elif event.type == "path_found": print("\n[Система] Алгоритм нашёл решение!") self.render(event.data.get("maze"), path=event.data.get("path")) elif event.type == "move": print( f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})") self.render(event.data.get("maze"), player_position=event.data.get("player_pos")) def render(self, maze, player_position=None, path=None) -> None: path_set = set(path) if path else set() for y in range(maze.height): row_chars = [] for x in range(maze.width): cell = maze.getCell(x, y) if player_position and cell == player_position: row_chars.append("P") elif cell.isStart: row_chars.append("S") elif cell.isExit: row_chars.append("E") elif cell in path_set: row_chars.append(".") elif cell.isWall: row_chars.append("#") else: row_chars.append(" ") print("".join(row_chars)) \end{lstlisting} \section{Результаты} Таблицы замеров времени и посещенных клеток: \begin{table}[H] \centering \caption{Результаты экспериментального сравнения алгоритмов поиска пути} \label{tab:maze_benchmark} \begin{tabular}{llccc} \toprule \textbf{Лабиринт} & \textbf{Стратегия} & \textbf{Время (мс)} & \textbf{Посещено клеток} & \textbf{Длина пути} \\ \midrule \multirow{4}{*}{Маленький (10×10)} & BFS & 0.0516 & 28 & 15 \\ & DFS & 0.0275 & 15 & 15 \\ & A* & 0.0360 & 16 & 15 \\ & Дейкстра & 0.0722 & 28 & 15 \\ \midrule \multirow{4}{*}{Пустой (30×30)} & BFS & 1.1863 & 870 & 58 \\ & DFS & 1.5568 & 842 & 842 \\ & A* & 0.4405 & 113 & 58 \\ & Дейкстра & 2.8607 & 870 & 58 \\ \midrule \multirow{4}{*}{Без выхода (15×15)} & BFS & 0.2230 & 160 & 0 \\ & DFS & 0.2959 & 160 & 0 \\ & A* & 0.9378 & 160 & 0 \\ & Дейкстра & 0.4148 & 160 & 0 \\ \midrule \multirow{4}{*}{Средний (50×50)} & BFS & 3.2247 & 1779 & 95 \\ & DFS & 1.6985 & 873 & 873 \\ & A* & 0.7348 & 158 & 95 \\ & Дейкстра & 6.1264 & 1779 & 95 \\ \midrule \multirow{4}{*}{Большой (100×100)} & BFS & 10.1308 & 7320 & 195 \\ & DFS & 6.1878 & 3549 & 3549 \\ & A* & 2.8441 & 328 & 195 \\ & Дейкстра & 35.2250 & 7320 & 195 \\ \bottomrule \end{tabular} \end{table} Графики: \begin{figure}[H] \includegraphics[scale=0.6]{time.eps} \end{figure} \begin{figure}[H] \includegraphics[scale=0.6]{cells.eps} \end{figure} \section{Анализ эффективности} Так как в нашем лабиринте вес всех ребер равны 1, то Дейкстра выродился в Поиск в ширину. Также Дейкстра несколько медленнее из за дополнительных расчетов на сортировку стоимостей. Самым лучшим по скорости стал алгоритм А*. Он в среднем 3-4 раза быстрее поиска в ширину, так как на каждом шаге он выбирает самого оптимального соседа для каждого узла, а поиск в ширину проверяет всех соседей. В разработанной рекурсивной стратегии DFS метрика посещенных клеток совпадает с длиной пути, так как алгоритм фиксирует состояние успешно развернутого стека вызовов в момент достижения целевой точки. Все тупиковые ветви, из которых рекурсия вышла до момента нахождения exit, отсекаются архитектурой возврата флага True, что демонстрирует специфику работы рекурсивного бэктрекинга в Python \section{Выводы по ООП} В ходе выполнения лабораторной работы была спроектирована и реализована объектно-ориентированная система поиска пути в лабиринтах. Применение принципов ООП и паттернов проектирования GoF позволило полностью разделить зоны ответственности классов (принцип Single Responsibility) и обеспечить высокий уровень гибкости и расширяемости приложения. 1. Как паттерны помогли сделать код гибким и расширяемым \begin{itemize} \item Разделение логики построения и представления (Паттерн Builder): Процесс создания лабиринта инкапсулирован внутри класса TextFileMazeBuilder. Сам лабиринт (Maze) и алгоритмы поиска никак не завязаны на формат хранения данных. Если в будущем потребуется сменить текстовый формат .txt на структуру .json достаточно будет создать нового строителя, реализующего интерфейс MazeBuilder. \item Изоляция и динамическая смена алгоритмов (Паттерн Strategy): Каждый алгоритм обхода графа вынесен в отдельный класс-стратегию с единым интерфейсом PathfindingStrategy. Класс-оркестратор MazeSolver работает исключительно с абстракцией. \item Использование Observer позволило отделить вычислительную составляющую от графической. Maze SOlver никак не учитывает где и как будут отображаться данные, он только отдает сигнал о событиях. Это позволяет если нужно изменить графический инт6ерфейс. \end{itemize} \end{document}