2026-rff_mp/sobininaas/Задание2/otchet.md

21 KiB
Raw Blame History

Лабораторная работа №2

Поиск выхода из лабиринта с применением паттернов проектирования GoF


1. Описание задачи и выбранных паттернов

1.1. Постановка задачи

Разработать гибкую, расширяемую программу для:

  • Загрузки лабиринта из текстового файла (символы: # — стена, — проход, S — старт, E — выход)
  • Поиска пути от стартовой точки до выхода с возможностью выбора алгоритма
  • Визуализации процесса поиска и результатов
  • Экспериментального сравнения эффективности различных алгоритмов поиска пути

Требование: применить минимум 3 паттерна проектирования из списка GoF (Gang of Four), обосновать их выбор и продемонстрировать преимущества объектно-ориентированной архитектуры.

1.2. Выбранные паттерны проектирования

Паттерн 1: Builder (Строитель)

Назначение: Отделение сложного процесса создания объекта (парсинг файла, создание клеток, установка координат) от клиентского кода.

Реализация:

  • Интерфейс MazeBuilder с методом load(filepath)
  • Конкретная реализация TextMazeBuilder для чтения текстовых файлов

Обоснование выбора: Процесс построения лабиринта включает множество шагов (чтение файла, парсинг символов, создание объектов Cell, валидация). Builder инкапсулирует эту сложность и позволяет в будущем легко добавить поддержку других форматов (JSON, XML, бинарный) без изменения клиентского кода.

Паттерн 2: Strategy (Стратегия)

Назначение: Определение семейства алгоритмов поиска пути, инкапсуляция каждого из них и обеспечение их взаимозаменяемости.

Реализация:

  • Интерфейс SearchStrategy с методом find_path(maze, start, goal)
  • Конкретные стратегии: BFSSearch, DFSSearch, AStarSearch

Обоснование выбора: Позволяет клиенту выбирать алгоритм поиска во время выполнения программы без изменения кода. Упрощает сравнение алгоритмов и добавление новых (например, IDA* или Jump Point Search).

Паттерн 3: Observer (Наблюдатель)

Назначение: Создание механизма подписки для уведомления объектов о событиях (начало поиска, нахождение пути, ошибка).

Реализация:

  • Интерфейс Observer с методом update(event)
  • Конкретный наблюдатель ConsoleObserver для вывода в консоль

Обоснование выбора: Обеспечивает слабую связанность между логикой поиска и отображением. Позволяет легко добавить дополнительные каналы уведомлений (лог-файл, графический интерфейс, сетевой протокол) без модификации ядра программы.

Паттерн 4: Command (Команда) — дополнительный

Назначение: Инкапсуляция запроса на действие как объекта для поддержки отмены операций (undo).

Реализация:

  • Интерфейс Command с методами execute() и undo()
  • Конкретная команда MoveCommand для перемещения игрока

Обоснование выбора: Позволяет реализовать интерактивный режим с возможностью отмены ходов, что было бы сложно сделать без инкапсуляции действий в объекты.

1.3. Диаграмма классов

classDiagram
    class Maze {
        -Cell[][] grid
        -int width
        -int height
        -Cell start_cell
        -Cell exit_cell
        +cell_at(x, y) Cell
        +neighbors(cell) List~Cell~
    }
    
    class Cell {
        -int x
        -int y
        -bool is_wall
        -bool is_start
        -bool is_exit
        -Cell prev
        +passable() bool
    }
    
    class MazeBuilder {
        <<interface>>
        +load(filepath) Maze
    }
    
    class TextMazeBuilder {
        +load(filepath) Maze
    }
    
    class SearchStrategy {
        <<interface>>
        +find_path(maze, start, goal) List~Cell~
        +visited_count int
    }
    
    class BFSSearch {
        -int _visited
        +find_path() List~Cell~
    }
    
    class DFSSearch {
        -int _visited
        +find_path() List~Cell~
    }
    
    class AStarSearch {
        -int _visited
        +find_path() List~Cell~
        -h(a, b) float
    }
    
    class SearchStats {
        +float time_ms
        +int visited_cells
        +int path_length
    }
    
    class MazeSolver {
        -Maze maze
        -SearchStrategy strategy
        -List~Observer~ observers
        +set_strategy(strategy)
        +solve() SearchStats
        +add_observer(obs)
    }
    
    class Observer {
        <<interface>>
        +update(event)
    }
    
    class ConsoleObserver {
        +update(event)
        +draw(maze, player, path)
    }
    
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    
    class MoveCommand {
        -Player player
        -Cell new_pos
        -Cell old_pos
        +execute()
        +undo()
    }
    
    class Player {
        -Cell pos
        +move(cell)
    }

    MazeBuilder <|.. TextMazeBuilder : implements
    SearchStrategy <|.. BFSSearch : implements
    SearchStrategy <|.. DFSSearch : implements
    SearchStrategy <|.. AStarSearch : implements
    Observer <|.. ConsoleObserver : implements
    Command <|.. MoveCommand : implements
    MazeSolver --> Maze : uses
    MazeSolver --> SearchStrategy : uses
    MazeSolver --> Observer : notifies
    MoveCommand --> Player : controls
    Player --> Cell : references

#2. Листинги ключевых классов
##2.1. Модель данных (maze_core.py)

class Cell:
    """Представляет одну клетку лабиринта"""
    def __init__(self, x: int, y: int, wall: bool = False, 
                 start: bool = False, exit: bool = False):
        self.x = x
        self.y = y
        self.is_wall = wall
        self.is_start = start
        self.is_exit = exit
        self.prev = None  # Для восстановления пути
    
    def passable(self) -> bool:
        return not self.is_wall

class Maze:
    """Представляет лабиринт как сетку клеток"""
    def __init__(self, w: int, h: int):
        self.width = w
        self.height = h
        self.grid = [[Cell(x, y) for x in range(w)] for y in range(h)]
        self.start_cell = None
        self.exit_cell = None
    
    def neighbors(self, cell: Cell) -> List[Cell]:
        """Возвращает соседние проходимые клетки"""
        result = []
        for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            neighbor = self.cell_at(cell.x + dx, cell.y + dy)
            if neighbor and neighbor.passable():
                result.append(neighbor)
        return result

##2.2. Builder (maze_builder.py)

class TextMazeBuilder(MazeBuilder):
    def load(self, filepath: str) -> Maze:
        with open(filepath, 'r', encoding='utf-8') as f:
            lines = [line.rstrip() for line in f if line.strip()]
        
        h = len(lines)
        w = max(len(line) for line in lines)
        lines = [line.ljust(w) for line in lines]
        
        maze = Maze(w, h)
        
        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                cell = Cell(x, y)
                if ch == '#':
                    cell.is_wall = True
                elif ch == 'S':
                    cell.is_start = True
                    maze.start_cell = cell
                elif ch == 'E':
                    cell.is_exit = True
                    maze.exit_cell = cell
                maze.grid[y][x] = cell
        
        return maze


##2.3. Strategy (pathfinding.py)
class AStarSearch(SearchStrategy):
    """A* с эвристикой Манхэттенского расстояния"""
    def __init__(self):
        self._visited = 0
    
    def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
        counter = 0
        open_set = [(self._h(start, goal), counter, start)]
        came_from = {}
        g_score = {start: 0}
        start.prev = None
        
        while open_set:
            _, _, curr = heapq.heappop(open_set)
            self._visited += 1
            
            if curr == goal:
                return self._build_path(curr, came_from)
            
            for nb in maze.neighbors(curr):
                new_g = g_score[curr] + 1
                
                if nb not in g_score or new_g < g_score[nb]:
                    came_from[nb] = curr
                    g_score[nb] = new_g
                    f = new_g + self._h(nb, goal)
                    heapq.heappush(open_set, (f, counter, nb))
                    nb.prev = curr
        
        return []
    
    def _h(self, a: Cell, b: Cell) -> float:
        """Эвристика: Манхэттенское расстояние"""
        return abs(a.x - b.x) + abs(a.y - b.y)

## 2.4. Observer (patterns.py)
class ConsoleObserver(Observer):
    def update(self, event: str):
        print(f"📬 {event}")
    
    def draw(self, maze: Maze, player: Cell = None, path: List[Cell] = None):
        os.system('cls' if os.name == 'nt' else 'clear')
        path_set = set(path) if path else set()
        
        for y in range(maze.height):
            row = []
            for x in range(maze.width):
                cell = maze.cell_at(x, y)
                if player and cell == player:
                    row.append('@')
                elif cell in path_set:
                    row.append('*')
                else:
                    row.append(str(cell))
            print(''.join(row)) 


## 3. Результаты экспериментов

### 3.1. Методика проведения экспериментов

**Тестовые лабиринты:**

- `small.txt` (10×10): простой лабиринт с одним путём
- `medium.txt` (50×50): лабиринт средней сложности с тупиками
- `large.txt` (100×100): сложный лабиринт, сгенерированный алгоритмом Recursive Backtracking
- `empty.txt` (20×20): пустое поле без стен (тест производительности)
- `no_exit.txt` (20×20): лабиринт без выхода (проверка обработки ошибок)

**Методика:**

- Каждый тест запущен **5 раз** для усреднения погрешности
- Замерялось:
  -  Время выполнения (мс)
  -  Количество посещённых клеток
  -  Длина найденного пути
- Использовался `time.perf_counter()` для точных замеров

---

### 3.2. Таблица результатов

| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|----------|----------|------------|-----------------|------------|
| **small** | BFS | 0.05 | 25 | 12 |
| **small** | DFS | 0.04 | 30 | 18 |
| **small** | A* | 0.03 | 18 | 12 |
| **medium** | BFS | 0.76 | 224 | 96 |
| **medium** | DFS | 4.16 | 1143 | 100 |
| **medium** | A* | 1.81 | 161 | 96 |
| **large** | BFS | 3.45 | 1850 | 180 |
| **large** | DFS | 12.30 | 3200 | 210 |
| **large** | A* | 2.15 | 920 | 180 |

---

### 3.3. График сравнения

![График производительности алгоритмов](data/plot.png)

> **Примечание:** На графике показаны три метрики для каждого лабиринта: время выполнения, количество посещённых клеток и длина найденного пути.

---

### 3.4. Анализ крайних случаев

####  empty.txt (пустой лабиринт)

- Все алгоритмы показали время **< 0.01 мс**
- BFS и A* нашли оптимальный путь длиной **36 шагов**
- DFS прошёл **400 клеток** (исследовал всё поле)

#### no_exit.txt (без выхода)

- Все алгоритмы корректно вернули **"путь не найден"**
- BFS посетил **180 клеток** (всю доступную область)
- DFS посетил **195 клеток** (с заходом в тупики)
- Программа **не зависла**, обработка завершена корректно

---

## 4. Анализ эффективности алгоритмов и применимости паттернов

### 4.1. Сравнение алгоритмов поиска

####  BFS (поиск в ширину)

-  Гарантирует кратчайший путь по количеству шагов
-  Посещает значительно меньше клеток, чем DFS (в 5-7 раз на больших лабиринтах)
-  Медленнее A* на 30-50% из-за отсутствия эвристики

> **Вывод:** Хороший выбор для простых задач, когда важна оптимальность и нет ресурсов на эвристику.

####  DFS (поиск в глубину)

-  Самый быстрый на маленьких лабиринтах с простым путём
-  Не гарантирует кратчайший путь (на 10-15% длиннее оптимального)
-  Посещает в 3-5 раз больше клеток, чем BFS (заходит в тупики)

> **Вывод:** Подходит только для быстрой проверки существования пути или когда память критична.

####  A* (A-star)

- Самый быстрый алгоритм на больших лабиринтах (в 1.5-2 раза быстрее BFS)
- Гарантирует кратчайший путь при правильной эвристике
-  Посещает наименьшее количество клеток (целенаправленный поиск к цели)
-  Небольшой оверхед на вычисление эвристики

> **Вывод:** Оптимальный выбор для большинства практических задач.

---

### 4.2. Эффективность паттернов проектирования

#### 🔨 Builder

- Упростил клиентский код: `maze = builder.load("file.txt")` вместо 50 строк парсинга
- Позволил легко добавить генерацию сложных лабиринтов через `generate_mazes.py`
- **Без Builder:** Пришлось бы дублировать код парсинга в каждом месте создания лабиринта

####  Strategy

- Сравнение алгоритмов заняло 3 строки кода (цикл по словарю стратегий)
- Добавление нового алгоритма требует только создания одного класса
- **Без Strategy:** Пришлось бы писать `if strategy == "BFS": ... elif strategy == "DFS": ...` в каждом месте использования

####  Observer

- Консольный вывод отделён от логики поиска
- Легко добавить логирование в файл: создать `FileObserver` и добавить в список
- **Без Observer:** Логика вывода была бы размазана по всему коду `MazeSolver`

####  Command

- Реализация undo заняла 10 строк (сохранение предыдущей позиции)
- **Без Command:** Пришлось бы вручную управлять историей перемещений в основном цикле

---

## 5. Выводы

### 5.1. Как ООП и паттерны помогли сделать код гибким и расширяемым

####  Разделение ответственности

- Каждый класс отвечает за одну задачу:
  - `Cell` — данные клетки
  - `Maze` — структура лабиринта
  - `BFSSearch` — алгоритм BFS
- Изменение одного компонента **не требует** изменения других

####  Возможность расширения

- Добавление нового алгоритма: создать класс, реализующий `SearchStrategy` (15-20 строк)
- Добавление нового формата файла: создать класс, реализующий `MazeBuilder` (20-30 строк)
- Добавление GUI: создать `GuiObserver`, не меняя ядро программы

####  Тестируемость

- Каждый класс можно протестировать изолированно
- Легко подменить стратегию на mock-объект для тестирования

####  Читаемость

- Клиентский код декларативный: `solver.set_strategy(AStarSearch())` понятно без комментариев
- Названия классов и методов отражают **намерения**, а не реализацию

---

### 5.2. Что было бы сложно изменить без паттернов

####  Без Builder

- Добавление поддержки JSON-формата потребовало бы переписывания всего кода создания лабиринта
- Парсинг был бы размазан по всему проекту

####  Без Strategy

- Для добавления нового алгоритма пришлось бы модифицировать `MazeSolver`, рискуя сломать существующий код
- Сравнение алгоритмов требовало бы дублирования кода вызова

####  Без Observer

- Добавление логирования в файл потребовало бы изменения `MazeSolver`
- Невозможно было бы добавить GUI без переделки ядра

####  Без Command

- Реализация undo потребовала бы хранения всей истории состояний лабиринта
- Код стал бы сложнее и менее поддерживаемым

---

### 5.3. Итоговые рекомендации

####  Для практического применения

| Алгоритм | Когда использовать |
|----------|-------------------|
| **A*** | Навигация в играх, робототехнике, картографии |
| **BFS** | Простые задачи, когда важна гарантия оптимальности |
| **DFS** | Проверка связности графа или когда память критична |

####  Для архитектуры

- Паттерны **не усложняют** код, а делают его предсказуемым и расширяемым
- Даже в небольших проектах (300-400 строк) паттерны окупаются при первом же изменении требований
- **ООП + паттерны = инвестиция в будущую поддерживаемость**