Стратегия
Также известен как: Strategy
Стратегия — это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.

Проблема
Вы решили написать приложение-навигатор для путешественников. Оно должно показывать красивую и удобную карту, позволяющую с лёгкостью ориентироваться в незнакомом городе.
Одной из самых востребованных функций являлся поиск и прокладывание маршрутов. Пребывая в неизвестном ему городе, пользователь должен иметь возможность указать начальную точку и пункт назначения, а навигатор — проложит оптимальный путь.
Первая версия вашего навигатора могла прокладывать маршрут лишь по дорогам, поэтому отлично подходила для путешествий на автомобиле. Но, очевидно, не все ездят в отпуск на машине. Поэтому следующим шагом вы добавили в навигатор прокладывание пеших маршрутов.
Через некоторое время выяснилось, что некоторые люди предпочитают ездить по городу на общественном транспорте. Поэтому вы добавили и такую опцию прокладывания пути.
Но и это ещё не всё. В ближайшей перспективе вы хотели бы добавить прокладывание маршрутов по велодорожкам. А в отдалённом будущем — интересные маршруты посещения достопримечательностей.

Код навигатора становится слишком раздутым.
Если с популярностью навигатора не было никаких проблем, то техническая часть вызывала вопросы и периодическую головную боль. С каждым новым алгоритмом код основного класса навигатора увеличивался вдвое. В таком большом классе стало довольно трудно ориентироваться.
Любое изменение алгоритмов поиска, будь то исправление багов или добавление нового алгоритма, затрагивало основной класс. Это повышало риск сделать ошибку, случайно задев остальной работающий код.
Кроме того, осложнялась командная работа с другими программистами, которых вы наняли после успешного релиза навигатора. Ваши изменения нередко затрагивали один и тот же код, создавая конфликты, которые требовали дополнительного времени на их разрешение.
Решение
Паттерн Стратегия предлагает определить семейство схожих алгоритмов, которые часто изменяются или расширяются, и вынести их в собственные классы, называемые стратегиями.
Вместо того, чтобы изначальный класс сам выполнял тот или иной алгоритм, он будет играть роль контекста, ссылаясь на одну из стратегий и делегируя ей выполнение работы. Чтобы сменить алгоритм, вам будет достаточно подставить в контекст другой объект-стратегию.
Важно, чтобы все стратегии имели общий интерфейс. Используя этот интерфейс, контекст будет независимым от конкретных классов стратегий. С другой стороны, вы сможете изменять и добавлять новые виды алгоритмов, не трогая код контекста.

Стратегии построения пути.
В нашем примере каждый алгоритм поиска пути переедет в свой собственный класс. В этих классах будет определён лишь один метод, принимающий в параметрах координаты начала и конца пути, а возвращающий массив точек маршрута.
Хотя каждый класс будет прокладывать маршрут по-своему, для навигатора это не будет иметь никакого значения, так как его работа заключается только в отрисовке маршрута. Навигатору достаточно подать в стратегию данные о начале и конце маршрута, чтобы получить массив точек маршрута в оговорённом формате.
Класс навигатора будет иметь метод для установки стратегии, позволяя изменять стратегию поиска пути на лету. Такой метод пригодится клиентскому коду навигатора, например, переключателям типов маршрутов в пользовательском интерфейсе.
Аналогия из жизни

Различные стратегии попадания в аэропорт.
Вам нужно добраться до аэропорта. Можно доехать на автобусе, такси или велосипеде. Здесь вид транспорта является стратегией. Вы выбираете конкретную стратегию в зависимости от контекста — наличия денег или времени до отлёта.
Структура

- Контекст хранит ссылку на объект конкретной стратегии, работая с ним через общий интерфейс стратегий.
- Стратегия определяет интерфейс, общий для всех вариаций алгоритма. Контекст использует этот интерфейс для вызова алгоритма.
Для контекста неважно, какая именно вариация алгоритма будет выбрана, так как все они имеют одинаковый интерфейс. - Конкретные стратегии реализуют различные вариации алгоритма.
- Во время выполнения программы контекст получает вызовы от клиента и делегирует их объекту конкретной стратегии.
- Клиент должен создать объект конкретной стратегии и передать его в конструктор контекста. Кроме этого, клиент должен иметь возможность заменить стратегию на лету, используя сеттер. Благодаря этому, контекст не будет знать о том, какая именно стратегия сейчас выбрана.
Псевдокод
В этом примере контекст использует Стратегию для выполнения той или иной арифметической операции.
Применимость
Когда вам нужно использовать разные вариации какого-то алгоритма внутри одного объекта.
Стратегия позволяет варьировать поведение объекта во время выполнения программы, подставляя в него различные объекты-поведения (например, отличающиеся балансом скорости и потребления ресурсов).
Когда у вас есть множество похожих классов, отличающихся только некоторым поведением.
Стратегия позволяет вынести отличающееся поведение в отдельную иерархию классов, а затем свести первоначальные классы к одному, сделав поведение этого класса настраиваемым.
Когда вы не хотите обнажать детали реализации алгоритмов для других классов.
Стратегия позволяет изолировать код, данные и зависимости алгоритмов от других объектов, скрыв эти детали внутри классов-стратегий.
Когда различные вариации алгоритмов реализованы в виде развесистого условного оператора. Каждая ветка такого оператора представляет собой вариацию алгоритма.
Стратегия помещает каждую лапу такого оператора в отдельный класс-стратегию. Затем контекст получает определённый объект-стратегию от клиента и делегирует ему работу. Если вдруг понадобится сменить алгоритм, в контекст можно подать другую стратегию.
Шаги реализации
- Определите алгоритм, который подвержен частым изменениям. Также подойдёт алгоритм, имеющий несколько вариаций, которые выбираются во время выполнения программы.
- Создайте интерфейс стратегий, описывающий этот алгоритм. Он должен быть общим для всех вариантов алгоритма.
- Поместите вариации алгоритма в собственные классы, которые реализуют этот интерфейс.
- В классе контекста создайте поле для хранения ссылки на текущий объект-стратегию, а также метод для её изменения. Убедитесь в том, что контекст работает с этим объектом только через общий интерфейс стратегий.
- Клиенты контекста должны подавать в него соответствующий объект-стратегию, когда хотят, чтобы контекст вёл себя определённым образом.
Преимущества и недостатки
Преимущества
- Горячая замена алгоритмов на лету.
- Изолирует код и данные алгоритмов от остальных классов.
- Уход от наследования к делегированию.
- Реализует принцип открытости/закрытости.
Недостатки
- Усложняет программу за счёт дополнительных классов.
- Клиент должен знать, в чём состоит разница между стратегиями, чтобы выбрать подходящую.
Примеры реализации паттерна
Python
- Сложность: 1/3
- Популярность: 3/3
- Применимость: Стратегия часто используется в Python-коде, особенно там, где нужно подменять алгоритм во время выполнения программы. Многие примеры стратегии можно заменить простыми lambda-выражениями.
- Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.
Этот пример показывает структуру паттерна Стратегия, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e
Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a
PHP
- Сложность: 1/3
- Популярность: 3/3
- Применимость: Стратегию часто используют в PHP-коде, особенно там, где нужно подменять алгоритм во время выполнения программы. Но у паттерна есть довольно сильный конкурент в лице анонимных функций, которые PHP уже довольно давно поддерживает.
- Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.
Этот пример показывает структуру паттерна Стратегия, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
В этом примере паттерн Стратегия используется для представления способов оплаты в приложении электронной коммерции.
Каждый способ оплаты может отображать форму оплаты для сбора надлежащих платёжных реквизитов пользователя и отправки его в компанию по обработке платежей. После того, как компания по обработке платежей перенаправляет пользователя обратно на сайт, метод оплаты проверяет возвращаемые параметры и помогает решить, был ли заказ завершён.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
Client: Let's create some orders
Controller: POST request to /orders with {"email":"[email protected]",
"product":"ABC Cat food (XL)","total":9.95}
Controller: Created the order #0.
Controller: POST request to /orders with {"email":"[email protected]",
"product":"XYZ Cat litter (XXL)","total":19.95}
Controller: Created the order #1.
Client: List my orders, please
Controller: GET request to /orders
Controller: Here's all orders:
{
"id": 0,
"status": "new",
"email": "[email protected]",
"product": "ABC Cat food (XL)",
"total": 9.95
}
{
"id": 1,
"status": "new",
"email": "[email protected]",
"product": "XYZ Cat litter (XXL)",
"total": 19.95
}
Client: I'd like to pay for the second, show me the payment form
Controller: GET request to /order/1/payment/paypal
Controller: here's the payment form:
<form action="https://paypal.com/payment" method="POST">
<input type="hidden" id="email" value="[email protected]">
<input type="hidden" id="total" value="19.95">
<input type="hidden" id="returnURL" value="https://our-website.com/order/1/payment/paypal/return">
<input type="submit" value="Pay on PayPal">
</form>
Client: ...pushes the Pay button...
Client: Oh, I'm redirected to the PayPal.
Client: ...pays on the PayPal...
Client: Alright, I'm back with you, guys.
Controller: GET request to /order/1/payment/paypal/return?key=c55a3964833a4b0fa4469ea94a057152&success=true&total=19.95
PayPalPayment: ...validating... Done!
Controller: Thanks for your order!
Order: #1 is now completed.
Задачи
Задача 1. Сортировщик с выбором алгоритма
Реализуйте класс Sorter, который сортирует список с помощью сменной стратегии. Реализуйте стратегии: BubbleSort, QuickSort, MergeSort. Контекст должен позволять менять стратегию на лету и измерять время выполнения каждой.
from __future__ import annotations
from abc import ABC, abstractmethod
import time
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
@property
@abstractmethod
def name(self) -> str: ...
class BubbleSort(SortStrategy):
@property
def name(self) -> str:
return "BubbleSort"
def sort(self, data: list) -> list:
# TODO: реализовать сортировку пузырьком
pass
class QuickSort(SortStrategy):
@property
def name(self) -> str:
return "QuickSort"
def sort(self, data: list) -> list:
# TODO: реализовать быструю сортировку (рекурсивно)
pass
class MergeSort(SortStrategy):
@property
def name(self) -> str:
return "MergeSort"
def sort(self, data: list) -> list:
# TODO: реализовать сортировку слиянием
pass
class Sorter:
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
self._strategy = strategy
print(f"[Sorter] Стратегия изменена на {strategy.name}")
def sort(self, data: list) -> list:
# TODO: выполнить сортировку, вывести название стратегии и время
pass
def benchmark(self, data: list,
strategies: list[SortStrategy]) -> dict[str, float]:
# TODO: запустить все стратегии на копии data, вернуть {name: time_ms}
pass
# Ожидаемый результат:
# data = [64, 34, 25, 12, 22, 11, 90]
# sorter = Sorter(BubbleSort())
# print(sorter.sort(data)) # [11, 12, 22, 25, 34, 64, 90]
# sorter.set_strategy(QuickSort())
# print(sorter.sort(data)) # [11, 12, 22, 25, 34, 64, 90]
# import random
# big = random.sample(range(10000), 1000)
# results = sorter.benchmark(big, [BubbleSort(), QuickSort(), MergeSort()])
# for name, ms in results.items():
# print(f"{name}: {ms:.2f}ms")
<?php
interface SortStrategy
{
public function sort(array $data): array;
public function getName(): string;
}
class BubbleSort implements SortStrategy
{
public function getName(): string { return "BubbleSort"; }
public function sort(array $data): array
{
// TODO: реализовать сортировку пузырьком
}
}
class QuickSort implements SortStrategy
{
public function getName(): string { return "QuickSort"; }
public function sort(array $data): array
{
// TODO: реализовать быструю сортировку
}
}
class MergeSort implements SortStrategy
{
public function getName(): string { return "MergeSort"; }
public function sort(array $data): array
{
// TODO: реализовать сортировку слиянием
}
}
class Sorter
{
private SortStrategy $strategy;
public function __construct(SortStrategy $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(SortStrategy $strategy): void
{
$this->strategy = $strategy;
echo "[Sorter] Стратегия изменена на {$strategy->getName()}\n";
}
public function sort(array $data): array
{
// TODO: выполнить сортировку, вывести название и время
}
public function benchmark(array $data, array $strategies): array
{
// TODO: запустить каждую стратегию, вернуть [name => time_ms]
}
}
// Ожидаемый результат:
// $data = [64, 34, 25, 12, 22, 11, 90];
// $sorter = new Sorter(new BubbleSort());
// print_r($sorter->sort($data)); // [11, 12, 22, 25, 34, 64, 90]
// $sorter->setStrategy(new QuickSort());
// print_r($sorter->sort($data)); // [11, 12, 22, 25, 34, 64, 90]
// $big = range(1, 1000); shuffle($big);
// $results = $sorter->benchmark($big, [new BubbleSort(), new QuickSort(), new MergeSort()]);
// foreach ($results as $name => $ms) { echo "$name: {$ms}ms\n"; }
Задача 2. Калькулятор скидок
Реализуйте систему расчёта скидок для интернет-магазина. Стратегии: NoDiscount (без скидки), PercentDiscount (процент от суммы), FixedDiscount (фиксированная сумма), BulkDiscount (скидка при кол-ве > N), LoyaltyDiscount (скидка по баллам лояльности). Контекст Cart автоматически выбирает лучшую стратегию.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class CartItem:
name: str
price: float
quantity: int
@property
def subtotal(self) -> float:
return self.price * self.quantity
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
"""Вернуть сумму скидки."""
...
@abstractmethod
def describe(self) -> str: ...
class NoDiscount(DiscountStrategy):
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
return 0.0
def describe(self) -> str:
return "Без скидки"
class PercentDiscount(DiscountStrategy):
def __init__(self, percent: float) -> None:
self.percent = percent
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
# TODO: процент от общей суммы
pass
def describe(self) -> str:
return f"Скидка {self.percent}%"
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float) -> None:
self.amount = amount
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
# TODO: фиксированная сумма, но не больше итога
pass
def describe(self) -> str:
return f"Скидка -{self.amount}"
class BulkDiscount(DiscountStrategy):
def __init__(self, min_quantity: int, percent: float) -> None:
self.min_quantity = min_quantity
self.percent = percent
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
# TODO: скидка если суммарное кол-во товаров >= min_quantity
pass
def describe(self) -> str:
return f"Скидка {self.percent}% при заказе от {self.min_quantity} шт."
class LoyaltyDiscount(DiscountStrategy):
POINTS_TO_CURRENCY = 0.01 # 1 балл = 0.01 единицы валюты
def __init__(self, max_points_to_use: int) -> None:
self.max_points = max_points_to_use
def calculate(self, items: list[CartItem],
loyalty_points: int = 0) -> float:
# TODO: использовать min(loyalty_points, max_points) * коэффициент
pass
def describe(self) -> str:
return f"Скидка по баллам лояльности (макс. {self.max_points} баллов)"
class Cart:
def __init__(self) -> None:
self._items: list[CartItem] = []
self._strategy: DiscountStrategy = NoDiscount()
self.loyalty_points = 0
def add_item(self, item: CartItem) -> None:
self._items.append(item)
def set_strategy(self, strategy: DiscountStrategy) -> None:
self._strategy = strategy
def set_best_strategy(self,
strategies: list[DiscountStrategy]) -> None:
# TODO: выбрать стратегию с максимальной скидкой
pass
@property
def subtotal(self) -> float:
return sum(i.subtotal for i in self._items)
@property
def discount(self) -> float:
return self._strategy.calculate(self._items, self.loyalty_points)
@property
def total(self) -> float:
return max(0.0, self.subtotal - self.discount)
def receipt(self) -> str:
lines = [f" {i.name} x{i.quantity} = {i.subtotal:.2f}"
for i in self._items]
lines.append(f"Подытог: {self.subtotal:.2f}")
lines.append(f"Скидка ({self._strategy.describe()}): -{self.discount:.2f}")
lines.append(f"Итого: {self.total:.2f}")
return "\n".join(lines)
# Ожидаемый результат:
# cart = Cart()
# cart.add_item(CartItem("Apple", 1.0, 10))
# cart.add_item(CartItem("Milk", 2.5, 3))
# cart.loyalty_points = 500
# strategies = [
# NoDiscount(),
# PercentDiscount(10),
# FixedDiscount(3.0),
# BulkDiscount(min_quantity=10, percent=15),
# LoyaltyDiscount(max_points_to_use=300),
# ]
# cart.set_best_strategy(strategies)
# print(cart.receipt())
<?php
class CartItem
{
public function __construct(
public readonly string $name,
public readonly float $price,
public readonly int $quantity,
) {}
public function getSubtotal(): float
{
return $this->price * $this->quantity;
}
}
interface DiscountStrategy
{
public function calculate(array $items, int $loyaltyPoints = 0): float;
public function describe(): string;
}
class NoDiscount implements DiscountStrategy
{
public function calculate(array $items, int $loyaltyPoints = 0): float { return 0.0; }
public function describe(): string { return "Без скидки"; }
}
class PercentDiscount implements DiscountStrategy
{
public function __construct(private readonly float $percent) {}
public function calculate(array $items, int $loyaltyPoints = 0): float
{
// TODO: процент от общей суммы
}
public function describe(): string { return "Скидка {$this->percent}%"; }
}
class FixedDiscount implements DiscountStrategy
{
public function __construct(private readonly float $amount) {}
public function calculate(array $items, int $loyaltyPoints = 0): float
{
// TODO: фиксированная сумма, но не больше итога
}
public function describe(): string { return "Скидка -{$this->amount}"; }
}
class BulkDiscount implements DiscountStrategy
{
public function __construct(
private readonly int $minQuantity,
private readonly float $percent,
) {}
public function calculate(array $items, int $loyaltyPoints = 0): float
{
// TODO: скидка если суммарное кол-во >= minQuantity
}
public function describe(): string
{
return "Скидка {$this->percent}% при заказе от {$this->minQuantity} шт.";
}
}
class LoyaltyDiscount implements DiscountStrategy
{
private const POINTS_TO_CURRENCY = 0.01;
public function __construct(private readonly int $maxPoints) {}
public function calculate(array $items, int $loyaltyPoints = 0): float
{
// TODO: min(loyaltyPoints, maxPoints) * коэффициент
}
public function describe(): string
{
return "Скидка по баллам (макс. {$this->maxPoints})";
}
}
class Cart
{
private array $items = [];
private DiscountStrategy $strategy;
public int $loyaltyPoints = 0;
public function __construct()
{
$this->strategy = new NoDiscount();
}
public function addItem(CartItem $item): void { $this->items[] = $item; }
public function setStrategy(DiscountStrategy $strategy): void { $this->strategy = $strategy; }
public function setBestStrategy(array $strategies): void
{
// TODO: выбрать стратегию с максимальной скидкой
}
public function getSubtotal(): float
{
return array_sum(array_map(fn($i) => $i->getSubtotal(), $this->items));
}
public function getDiscount(): float
{
return $this->strategy->calculate($this->items, $this->loyaltyPoints);
}
public function getTotal(): float
{
return max(0.0, $this->getSubtotal() - $this->getDiscount());
}
public function receipt(): string
{
$lines = array_map(
fn($i) => " {$i->name} x{$i->quantity} = {$i->getSubtotal()}",
$this->items
);
$lines[] = "Подытог: " . $this->getSubtotal();
$lines[] = "Скидка ({$this->strategy->describe()}): -" . $this->getDiscount();
$lines[] = "Итого: " . $this->getTotal();
return implode("\n", $lines);
}
}
// Ожидаемый результат:
// $cart = new Cart();
// $cart->addItem(new CartItem("Apple", 1.0, 10));
// $cart->addItem(new CartItem("Milk", 2.5, 3));
// $cart->loyaltyPoints = 500;
// $strategies = [
// new NoDiscount(),
// new PercentDiscount(10),
// new FixedDiscount(3.0),
// new BulkDiscount(10, 15),
// new LoyaltyDiscount(300),
// ];
// $cart->setBestStrategy($strategies);
// echo $cart->receipt();
Задача 3. Экспортер отчётов
Реализуйте систему экспорта отчётов в разные форматы. Контекст ReportExporter принимает данные (список строк-записей) и экспортирует через сменную стратегию. Стратегии: CsvExporter, JsonExporter, XmlExporter, MarkdownTableExporter. Добавьте возможность экспорта сразу в несколько форматов.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class ReportRow:
id: int
name: str
value: float
category: str
class ExportStrategy(ABC):
@abstractmethod
def export(self, rows: list[ReportRow], title: str = "Report") -> str: ...
@property
@abstractmethod
def extension(self) -> str: ...
class CsvExporter(ExportStrategy):
@property
def extension(self) -> str:
return "csv"
def export(self, rows: list[ReportRow], title: str = "Report") -> str:
# TODO: вернуть CSV-строку с заголовком id,name,value,category
pass
class JsonExporter(ExportStrategy):
@property
def extension(self) -> str:
return "json"
def export(self, rows: list[ReportRow], title: str = "Report") -> str:
# TODO: вернуть JSON {"title": ..., "rows": [...]}
pass
class XmlExporter(ExportStrategy):
@property
def extension(self) -> str:
return "xml"
def export(self, rows: list[ReportRow], title: str = "Report") -> str:
# TODO: вернуть XML-строку
pass
class MarkdownTableExporter(ExportStrategy):
@property
def extension(self) -> str:
return "md"
def export(self, rows: list[ReportRow], title: str = "Report") -> str:
# TODO: вернуть Markdown-таблицу с заголовком ## title
pass
class ReportExporter:
def __init__(self, strategy: ExportStrategy) -> None:
self._strategy = strategy
def set_strategy(self, strategy: ExportStrategy) -> None:
self._strategy = strategy
def export(self, rows: list[ReportRow],
title: str = "Report") -> str:
# TODO: делегировать стратегии и вернуть результат
pass
def export_all(self, rows: list[ReportRow],
strategies: list[ExportStrategy],
title: str = "Report") -> dict[str, str]:
# TODO: вернуть {extension: content} для каждой стратегии
pass
# Ожидаемый результат:
# rows = [
# ReportRow(1, "Alice", 1500.0, "Sales"),
# ReportRow(2, "Bob", 2300.5, "IT"),
# ReportRow(3, "Carol", 1800.0, "Sales"),
# ]
# exporter = ReportExporter(CsvExporter())
# print(exporter.export(rows, "Q1 Report"))
# # id,name,value,category
# # 1,Alice,1500.0,Sales
# # ...
# exporter.set_strategy(MarkdownTableExporter())
# print(exporter.export(rows, "Q1 Report"))
# results = exporter.export_all(rows, [CsvExporter(), JsonExporter(), XmlExporter()], "Q1")
# print(list(results.keys())) # ['csv', 'json', 'xml']
<?php
class ReportRow
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly float $value,
public readonly string $category,
) {}
}
interface ExportStrategy
{
public function export(array $rows, string $title = "Report"): string;
public function getExtension(): string;
}
class CsvExporter implements ExportStrategy
{
public function getExtension(): string { return "csv"; }
public function export(array $rows, string $title = "Report"): string
{
// TODO: вернуть CSV-строку с заголовком id,name,value,category
}
}
class JsonExporter implements ExportStrategy
{
public function getExtension(): string { return "json"; }
public function export(array $rows, string $title = "Report"): string
{
// TODO: вернуть JSON {"title": ..., "rows": [...]}
}
}
class XmlExporter implements ExportStrategy
{
public function getExtension(): string { return "xml"; }
public function export(array $rows, string $title = "Report"): string
{
// TODO: вернуть XML-строку
}
}
class MarkdownTableExporter implements ExportStrategy
{
public function getExtension(): string { return "md"; }
public function export(array $rows, string $title = "Report"): string
{
// TODO: вернуть Markdown-таблицу с заголовком ## title
}
}
class ReportExporter
{
private ExportStrategy $strategy;
public function __construct(ExportStrategy $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(ExportStrategy $strategy): void
{
$this->strategy = $strategy;
}
public function export(array $rows, string $title = "Report"): string
{
// TODO: делегировать стратегии
}
public function exportAll(array $rows, array $strategies,
string $title = "Report"): array
{
// TODO: вернуть [extension => content]
}
}
// Ожидаемый результат:
// $rows = [
// new ReportRow(1, "Alice", 1500.0, "Sales"),
// new ReportRow(2, "Bob", 2300.5, "IT"),
// new ReportRow(3, "Carol", 1800.0, "Sales"),
// ];
// $exporter = new ReportExporter(new CsvExporter());
// echo $exporter->export($rows, "Q1 Report");
// // id,name,value,category
// // 1,Alice,1500.0,Sales
// // ...
// $exporter->setStrategy(new MarkdownTableExporter());
// echo $exporter->export($rows, "Q1 Report");
// $results = $exporter->exportAll($rows, [new CsvExporter(), new JsonExporter(), new XmlExporter()], "Q1");
// print_r(array_keys($results)); // ['csv', 'json', 'xml']
Задача 4. Навигатор с маршрутными стратегиями
Реализуйте навигатор, прокладывающий маршруты между точками с помощью сменных стратегий. Стратегии: CarRoute (по дорогам, скорость 60 км/ч), WalkingRoute (пешком, 5 км/ч), BicycleRoute (велосипед, 15 км/ч), PublicTransportRoute (транспорт, с пересадками). Контекст выбирает стратегию по предпочтениям пользователя.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
import math
@dataclass(frozen=True)
class Point:
lat: float
lon: float
name: str = ""
def distance_to(self, other: "Point") -> float:
"""Расстояние в километрах (упрощённая формула)."""
dlat = (other.lat - self.lat) * 111.0
dlon = (other.lon - self.lon) * 111.0 * math.cos(math.radians(self.lat))
return math.sqrt(dlat ** 2 + dlon ** 2)
@dataclass
class Route:
points: list[Point]
distance_km: float
duration_min: float
description: str
class RoutingStrategy(ABC):
@abstractmethod
def build_route(self, origin: Point, destination: Point) -> Route: ...
@property
@abstractmethod
def mode(self) -> str: ...
class CarRoute(RoutingStrategy):
SPEED_KMH = 60.0
@property
def mode(self) -> str:
return "Автомобиль"
def build_route(self, origin: Point, destination: Point) -> Route:
# TODO: прямой маршрут, скорость SPEED_KMH
pass
class WalkingRoute(RoutingStrategy):
SPEED_KMH = 5.0
@property
def mode(self) -> str:
return "Пешком"
def build_route(self, origin: Point, destination: Point) -> Route:
# TODO: пешеходный маршрут
pass
class BicycleRoute(RoutingStrategy):
SPEED_KMH = 15.0
@property
def mode(self) -> str:
return "Велосипед"
def build_route(self, origin: Point, destination: Point) -> Route:
# TODO: велосипедный маршрут
pass
class PublicTransportRoute(RoutingStrategy):
def __init__(self, stops: list[Point]) -> None:
self.stops = stops # промежуточные остановки
@property
def mode(self) -> str:
return "Общественный транспорт"
def build_route(self, origin: Point, destination: Point) -> Route:
# TODO: маршрут через stops, скорость 30 км/ч + 5 мин на каждую пересадку
pass
class Navigator:
def __init__(self, strategy: RoutingStrategy) -> None:
self._strategy = strategy
def set_strategy(self, strategy: RoutingStrategy) -> None:
self._strategy = strategy
print(f"[Navigator] Режим: {strategy.mode}")
def navigate(self, origin: Point, destination: Point) -> Route:
# TODO: построить маршрут и вывести информацию
pass
def compare_routes(self, origin: Point, destination: Point,
strategies: list[RoutingStrategy]) -> None:
# TODO: вывести сравнительную таблицу всех стратегий
pass
# Ожидаемый результат:
# home = Point(55.751, 37.618, "Дом")
# work = Point(55.796, 37.537, "Работа")
# nav = Navigator(CarRoute())
# route = nav.navigate(home, work)
# print(f"{route.description}: {route.distance_km:.1f} км, {route.duration_min:.0f} мин")
# nav.set_strategy(WalkingRoute())
# nav.compare_routes(home, work, [CarRoute(), WalkingRoute(), BicycleRoute()])
<?php
class Point
{
public function __construct(
public readonly float $lat,
public readonly float $lon,
public readonly string $name = "",
) {}
public function distanceTo(Point $other): float
{
$dlat = ($other->lat - $this->lat) * 111.0;
$dlon = ($other->lon - $this->lon) * 111.0 * cos(deg2rad($this->lat));
return sqrt($dlat ** 2 + $dlon ** 2);
}
}
class Route
{
public function __construct(
public readonly array $points,
public readonly float $distanceKm,
public readonly float $durationMin,
public readonly string $description,
) {}
}
interface RoutingStrategy
{
public function buildRoute(Point $origin, Point $destination): Route;
public function getMode(): string;
}
class CarRoute implements RoutingStrategy
{
private const SPEED = 60.0;
public function getMode(): string { return "Автомобиль"; }
public function buildRoute(Point $origin, Point $destination): Route
{
// TODO: прямой маршрут, скорость SPEED км/ч
}
}
class WalkingRoute implements RoutingStrategy
{
private const SPEED = 5.0;
public function getMode(): string { return "Пешком"; }
public function buildRoute(Point $origin, Point $destination): Route
{
// TODO: пешеходный маршрут
}
}
class BicycleRoute implements RoutingStrategy
{
private const SPEED = 15.0;
public function getMode(): string { return "Велосипед"; }
public function buildRoute(Point $origin, Point $destination): Route
{
// TODO: велосипедный маршрут
}
}
class PublicTransportRoute implements RoutingStrategy
{
public function __construct(private readonly array $stops = []) {}
public function getMode(): string { return "Общественный транспорт"; }
public function buildRoute(Point $origin, Point $destination): Route
{
// TODO: маршрут через stops, 30 км/ч + 5 мин на пересадку
}
}
class Navigator
{
private RoutingStrategy $strategy;
public function __construct(RoutingStrategy $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(RoutingStrategy $strategy): void
{
$this->strategy = $strategy;
echo "[Navigator] Режим: {$strategy->getMode()}\n";
}
public function navigate(Point $origin, Point $destination): Route
{
// TODO: построить маршрут и вывести информацию
}
public function compareRoutes(Point $origin, Point $destination,
array $strategies): void
{
// TODO: вывести сравнительную таблицу
}
}
// Ожидаемый результат:
// $home = new Point(55.751, 37.618, "Дом");
// $work = new Point(55.796, 37.537, "Работа");
// $nav = new Navigator(new CarRoute());
// $route = $nav->navigate($home, $work);
// echo "{$route->description}: {$route->distanceKm} км, {$route->durationMin} мин\n";
// $nav->compareRoutes($home, $work, [new CarRoute(), new WalkingRoute(), new BicycleRoute()]);
Задача 5. Валидатор с цепочкой стратегий
Реализуйте систему валидации данных пользователя. Каждое поле валидируется набором стратегий. Стратегии: RequiredValidator, LengthValidator, RegexValidator, RangeValidator, UniqueValidator. Контекст FieldValidator применяет стратегии последовательно и собирает все ошибки.
from __future__ import annotations
from abc import ABC, abstractmethod
import re
class ValidationStrategy(ABC):
@abstractmethod
def validate(self, value, context: dict | None = None) -> str | None:
"""Вернуть сообщение об ошибке или None если валидно."""
...
class RequiredValidator(ValidationStrategy):
def validate(self, value, context: dict | None = None) -> str | None:
# TODO: вернуть ошибку если value пустое (None, "", [], 0)
pass
class LengthValidator(ValidationStrategy):
def __init__(self, min_len: int = 0,
max_len: int | None = None) -> None:
self.min_len = min_len
self.max_len = max_len
def validate(self, value, context: dict | None = None) -> str | None:
# TODO: проверить длину строки
pass
class RegexValidator(ValidationStrategy):
def __init__(self, pattern: str, message: str) -> None:
self._pattern = re.compile(pattern)
self.message = message
def validate(self, value, context: dict | None = None) -> str | None:
# TODO: проверить совпадение с pattern
pass
class RangeValidator(ValidationStrategy):
def __init__(self, min_val: float | None = None,
max_val: float | None = None) -> None:
self.min_val = min_val
self.max_val = max_val
def validate(self, value, context: dict | None = None) -> str | None:
# TODO: проверить числовое значение в диапазоне
pass
class UniqueValidator(ValidationStrategy):
def __init__(self, existing: set) -> None:
self.existing = existing
def validate(self, value, context: dict | None = None) -> str | None:
# TODO: проверить отсутствие value в existing
pass
class FieldValidator:
def __init__(self, field_name: str) -> None:
self.field_name = field_name
self._strategies: list[ValidationStrategy] = []
def add(self, strategy: ValidationStrategy) -> "FieldValidator":
# TODO: добавить стратегию, вернуть self для цепочки
pass
def validate(self, value,
context: dict | None = None) -> list[str]:
# TODO: применить все стратегии, собрать список ошибок
pass
def is_valid(self, value, context: dict | None = None) -> bool:
return len(self.validate(value, context)) == 0
class FormValidator:
def __init__(self) -> None:
self._fields: dict[str, FieldValidator] = {}
def add_field(self, validator: FieldValidator) -> "FormValidator":
self._fields[validator.field_name] = validator
return self
def validate(self, data: dict) -> dict[str, list[str]]:
# TODO: валидировать каждое поле, вернуть {field: [errors]}
pass
def is_valid(self, data: dict) -> bool:
return all(len(e) == 0 for e in self.validate(data).values())
# Ожидаемый результат:
# existing_emails = {"[email protected]", "[email protected]"}
# form = FormValidator()
# form.add_field(
# FieldValidator("username")
# .add(RequiredValidator())
# .add(LengthValidator(min_len=3, max_len=20))
# .add(RegexValidator(r"^[a-zA-Z0-9_]+$", "только буквы, цифры и _"))
# ).add_field(
# FieldValidator("email")
# .add(RequiredValidator())
# .add(RegexValidator(r"^[^@]+@[^@]+\.[^@]+$", "неверный формат email"))
# .add(UniqueValidator(existing_emails))
# ).add_field(
# FieldValidator("age")
# .add(RequiredValidator())
# .add(RangeValidator(min_val=18, max_val=120))
# )
# errors = form.validate({"username": "Al", "email": "[email protected]", "age": 15})
# print(errors)
# # {'username': ['длина должна быть от 3 до 20 символов'],
# # 'email': ['значение уже занято'],
# # 'age': ['значение должно быть не менее 18']}
<?php
interface ValidationStrategy
{
/** Вернуть сообщение об ошибке или null если валидно. */
public function validate(mixed $value, array $context = []): ?string;
}
class RequiredValidator implements ValidationStrategy
{
public function validate(mixed $value, array $context = []): ?string
{
// TODO: ошибка если пустое значение
}
}
class LengthValidator implements ValidationStrategy
{
public function __construct(
private readonly int $minLen = 0,
private readonly ?int $maxLen = null,
) {}
public function validate(mixed $value, array $context = []): ?string
{
// TODO: проверить длину строки
}
}
class RegexValidator implements ValidationStrategy
{
public function __construct(
private readonly string $pattern,
private readonly string $message,
) {}
public function validate(mixed $value, array $context = []): ?string
{
// TODO: проверить совпадение с pattern
}
}
class RangeValidator implements ValidationStrategy
{
public function __construct(
private readonly ?float $minVal = null,
private readonly ?float $maxVal = null,
) {}
public function validate(mixed $value, array $context = []): ?string
{
// TODO: проверить числовое значение в диапазоне
}
}
class UniqueValidator implements ValidationStrategy
{
public function __construct(private readonly array $existing) {}
public function validate(mixed $value, array $context = []): ?string
{
// TODO: проверить отсутствие в existing
}
}
class FieldValidator
{
private array $strategies = [];
public function __construct(private readonly string $fieldName) {}
public function getFieldName(): string { return $this->fieldName; }
public function add(ValidationStrategy $strategy): static
{
// TODO: добавить стратегию, вернуть $this
}
public function validate(mixed $value, array $context = []): array
{
// TODO: применить все стратегии, собрать ошибки
}
public function isValid(mixed $value, array $context = []): bool
{
return empty($this->validate($value, $context));
}
}
class FormValidator
{
private array $fields = [];
public function addField(FieldValidator $validator): static
{
$this->fields[$validator->getFieldName()] = $validator;
return $this;
}
public function validate(array $data): array
{
// TODO: валидировать все поля, вернуть [field => [errors]]
}
public function isValid(array $data): bool
{
return empty(array_filter($this->validate($data)));
}
}
// Ожидаемый результат:
// $existingEmails = ["[email protected]", "[email protected]"];
// $form = (new FormValidator())
// ->addField(
// (new FieldValidator("username"))
// ->add(new RequiredValidator())
// ->add(new LengthValidator(minLen: 3, maxLen: 20))
// ->add(new RegexValidator('/^[a-zA-Z0-9_]+$/', "только буквы, цифры и _"))
// )
// ->addField(
// (new FieldValidator("email"))
// ->add(new RequiredValidator())
// ->add(new RegexValidator('/^[^@]+@[^@]+\.[^@]+$/', "неверный формат"))
// ->add(new UniqueValidator($existingEmails))
// )
// ->addField(
// (new FieldValidator("age"))
// ->add(new RequiredValidator())
// ->add(new RangeValidator(minVal: 18, maxVal: 120))
// );
// $errors = $form->validate(["username" => "Al", "email" => "[email protected]", "age" => 15]);
// print_r($errors);
// // ['username' => ['длина должна быть от 3 до 20'],
// // 'email' => ['значение уже занято'],
// // 'age' => ['значение должно быть не менее 18']]