2026-rff_mp/ProninVV/task-2-oop/report/document.tex

302 lines
13 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\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}