Неделя 5 создавала объекты, неделя 6 — компоновала их. А как объектам распределить ответственность и договориться о взаимодействии, не зная друг друга?
Этап недели 7 «Платёжной системы»: Strategy для комиссий + Command (undo) или Observer
Strategy — комиссии и валюты: фиксированная / процентная / комбинированная — взаимозаменяемые стратегии;
Command — pay/refund как команды с откатом, история с undo;
Все три паттерна — про то, как объекты договариваются о поведении, не зная друг друга.
Рекомендуемый набор проекта — Factory + Strategy + Command/Observer. Какой из трёх паттернов вы выберете для своего проекта — и сможете обосновать почему?
Не открывая конспект: что такое полиморфизм?
Закончите фразу: «один интерфейс — …».
Подсказка: вспомните — разные классы, один метод, разное поведение.
Пауза 10–15 секунд. Сегодня каждый паттерн — это полиморфизм + способ договориться о взаимодействии.
К концу лекции вы сможете…
устранять ветвления через Strategy и подставлять алгоритм снаружи;
строить связь «один ко многим» через Observer;
делать действия откладываемыми и отменяемыми через Command;
задавать скелет алгоритма через Template Method;
моделировать конечные автоматы через State;
использовать Iterator как встроенный паттерн.
Strategy и Observer — самые частые на собеседованиях и в промышленном коде. Обратите на них особое внимание.
Поведенческие паттерны — полиморфизм в действии: один интерфейс, разные реализации
Поведенческие паттерны распределяют ответственность между объектами и описывают, как они взаимодействуют.
Это прямое развитие полиморфизма из С2 М1: один интерфейс — разные реализации поведения.
Strategy и Observer — самые востребованные в промышленном Python; Command — основа транзакций и очередей задач.
Правило: сначала спросите «кто отвечает за поведение? кто с кем взаимодействует?» — ответ подскажет паттерн.
Если вы умеете полиморфизм, вы уже наполовину знаете поведенческие паттерны. Чего не хватает до целого? (способа взаимодействия)
Боль Strategy: if/elif по типу скидки правится при каждом новом тарифе
Связь с полиморфизмом: Strategy — «полиморфизм в действии»: контекст знает только интерфейс, конкретная стратегия подставляется снаружи.
Стратегия с внутренним состоянием там, где она должна быть чистой функцией;
контекст лезет в детали стратегии.
Смена стратегии в рантайме — сильный признак Strategy. Если выбор фиксирован — не усложняйте: хватит функции с параметром.
Боль Observer: Course знает всех получателей — email, лог, журнал
class Course:
defenroll(self, student: str) -> None:
self._students.append(student)
email.send(f"{student} записан") # жёсткая связь
logger.log(f"{student} записан") # ещё одна
journal.update(student) # и ещё одна
Боль: при каждом новом получателе (sms, telegram) правим Course; отправитель знает всех и как они реагируют; тестировать трудно.
Решение — субъект с подпиской: отправитель не знает получателей.
Что нужно поменять в Course, чтобы добавить SMS-уведомление? А если получателей станет пять? (добавить вызов — и Course раздувается)
CourseNotifier: субъект и подписчики «один ко многим»
Связь «один ко многим»: отправитель не знает, кто и как реагирует.
from typing import Callable
class CourseNotifier:
def__init__(self) -> None:
self._observers: list[Callable[[str], None]] = []
defsubscribe(self, observer: Callable[[str], None]) -> None:
self._observers.append(observer)
defnotify(self, message: str) -> None:
for observer in self._observers:
observer(message)
defemail(message: str) -> None:
print(f"email: {message}")
deflogger(message: str) -> None:
print(f"log: {message}")
notifier = CourseNotifier()
notifier.subscribe(email)
notifier.subscribe(logger)
notifier.notify("студент записан на курс")
email: студент записан на курс
log: студент записан на курс
Правило: добавление подписчика не меняет CourseNotifier; в Python наблюдатели часто — просто callable.
Что напечатается после notify? (два сообщения в порядке подписки: сначала email, потом log)
Observer: когда применять / когда не применять; отписка и изоляция ошибок
Когда применять
связь «один ко многим»;
независимые реакции на события;
событийная архитектура — база для асинхронности С3.
Когда не применять
один получатель — просто вызов метода;
получатели не меняются — обойдёмся без подписки.
Наблюдатель не отписывается — утечка ссылок и неожиданные вызовы;
исключение в одном наблюдателе рушит всех — нужно изолировать обработку ошибок;
порядок уведомлений не документирован.
Где «живёт» подписчик после того, как объект уже не нужен? (в списке наблюдателей — утечка ссылок; на семинаре проверим, что сбой одного не рушит остальных)
Боль Command: действие нужно отложить, отменить, залогировать
account.withdraw(200.0) # прямое действие# нельзя отложить, отменить, поставить в очередь
Боль: прямой вызов метода не позволяет отложить действие, поставить в очередь, отменить (undo), залогировать; вызывающий код знает исполнителя.
Решение — действие становится объектом с execute() и undo().
Как отменить только что сделанный withdraw? (нужен deposit — но кто запомнит сумму и порядок действий?)
Предскажите результат
PayCommand: execute() и undo() — действие стало объектом
Правило: команда хранит ровно свой контекст; вызывающий код не знает исполнителя; undo — обратное действие.
Чему равен баланс после execute? после undo? (800.0 и 1000.0 — команды можно складывать в стек истории, увидим в live-coding 2)
Command: когда применять / когда не применять; undo/redo, очередь, транзакции
Когда применять
undo/redo;
очередь задач, макрокоманды;
логирование действий;
транзакции.
Когда не применять
простое прямое действие — команда добавит лишние классы.
команда хранит слишком много контекста;
заявлен undo, но не реализован;
команды вызываются напрямую, минуя очередь/историю.
Downstream: Command — основа транзакций в БД и очередей задач в backend.
Сигнал к Command — слово «отменить» или «отложить» в требованиях. Если действие всегда мгновенно и необратимо — команда не нужна.
Стоп-вопрос
Стоп-вопрос: Strategy vs Template Method? Когда нужен Command?
Чем Strategy отличается от Template Method? (пока только по одному признаку — ответим полностью на Слайде 18)
Когда нужен Command, а не обычный вызов метода?
Эталон: Strategy — композиция, алгоритм подставляется снаружи; Template Method — наследование, скелет в базе; Command — когда действие нужно отложить/отменить/залогировать.
Вопрос всей группе, пауза. Если ответов меньше половины — полное различение TM/S на Слайде 18.
Боль Template Method: одинаковые шаги дублируются в каждом наследнике
class TextReport:
defgenerate(self) -> str:
data = self.collect_data()
body = self.render(data)
returnf"=== Отчёт ===\n{body}"# общий шагclass CsvReport:
defgenerate(self) -> str:
data = self.collect_data()
body = self.render(data)
returnf"=== Отчёт ===\n{body}"# тот же общий шаг
Боль: общий скелет (собрать → отрисовать → оформить) дублируется в каждом наследнике; изменение шапки отчёта требует правки всех классов.
Решение — скелет в базовом классе, вариативные шаги — в наследниках.
Что будет, если шапку отчёта поменять на "--- Отчёт ---"? (придётся править каждый класс)
Предскажите результат
ReportGenerator: скелет алгоритма в базе, вариативные шаги — в наследниках
Правило:generate задаёт последовательность шагов; наследники меняют только вариативные шаги; общий finalize не дублируется.
Что напечатает TextReport().generate()? («=== Отчёт ===» и «Студентов: 10»)
Template Method vs Strategy: наследование vs композиция; когда/не
Template Method — наследование
скелет алгоритма в базовом классе, наследники переопределяют шаги;
подходит, когда меняются шаги, а не весь алгоритм.
Strategy — композиция
алгоритм целиком подставляется снаружи;
подходит, когда меняется весь алгоритм.
Когда не применять Template Method: шаги не вариативны; если меняется весь алгоритм — предпочесть Strategy.
Слишком много абстрактных шагов — наследники переопределяют всё подряд.
«Каркас с дырками» vs «сменный модуль»: отчёт меняет шапку — какой паттерн? Скидка меняется целиком — какой? (TM — шаг finalize; Strategy — весь алгоритм)
Боль State: if state == ... расползается по всем методам
class Order:
def__init__(self) -> None:
self.state = "new"defpay(self) -> None:
if self.state == "new":
self.state = "paid"elif self.state == "paid":
raiseValueError("Уже оплачен")
elif self.state == "cancelled":
raiseValueError("Заказ отменён")
defcancel(self) -> None:
if self.state == "new":
self.state = "cancelled"elif self.state == "cancelled":
raiseValueError("Уже отменён")
# и так в каждом методе
Боль: ветвление if state == ... дублируется в каждом методе; добавление состояния требует править все методы; допустимые переходы не видны.
Решение — каждое состояние становится классом.
Сколько мест нужно править при добавлении состояния "refunded"? (все методы — это и есть «боль» State)
Предскажите результат
OrderState: каждое состояние — класс; переходы меняют объект состояния
class OrderState:
defpay(self, order) -> None: ...
defcancel(self, order) -> None: ...
class Order:
def__init__(self) -> None:
self.state: OrderState = NewOrder()
defpay(self) -> None:
self.state.pay(self)
defcancel(self) -> None:
self.state.cancel(self)
class NewOrder(OrderState):
defpay(self, order) -> None:
order.state = PaidOrder()
print("оплачен")
defcancel(self, order) -> None:
order.state = CancelledOrder()
print("отменён")
class PaidOrder(OrderState):
defpay(self, order) -> None:
raiseValueError("Заказ уже оплачен")
defcancel(self, order) -> None:
print("отмена оплаченного заказа")
class CancelledOrder(OrderState):
defpay(self, order) -> None:
raiseValueError("Отменённый заказ нельзя оплатить")
defcancel(self, order) -> None:
raiseValueError("Заказ уже отменён")
o = Order()
o.pay()
print(type(o.state).__name__)
оплачен
PaidOrder
Правило: контекст не содержит ветвлений — он делегирует текущему состоянию; переходы меняют объект состояния.
Что будет, если вызвать o.pay() повторно? (ValueError: Заказ уже оплачен — запрещённые переходы это исключения в классах состояний)
Какой переход запрещён, но часто забывается? (из «оплачен» в «новый» — возврата назад нет)
State: когда применять / когда не применять; 2–3 состояния — Enum проще
Когда применять
конечный автомат с явными состояниями и переходами;
один и тот же метод ведёт себя по-разному в разных состояниях.
Когда не применять
2–3 состояния без сложной логики — булев флаг или Enum проще.
состояния меняют друг друга напрямую, минуя контекст;
не проверяются допустимые переходы (из «оплачен» в «новый»).
State окупается, когда переходов много и они ветвятся; для пары флагов — не усложняйте.
Сколько состояний должно быть, чтобы State окупился? (много и ветвящихся переходов — иначе хватит Enum)
Iterator встроен в язык: __iter__ и генераторы — паттерн GoF «из коробки»
class Course:
def__init__(self, students: list[str]) -> None:
self._students = students
def__iter__(self):
yield from self._students
c = Course(["Иван", "Мария"])
for s in c:
print(s)
Иван
Мария
Правило:__iter__/__next__, генераторы, for — это Iterator GoF «из коробки» (С2 М3); итерируемый класс — паттерн без дополнительных классов; отдельный класс-итератор почти всегда заменяется генератором.
Вы уже писали __iter__ в С2 М3 — значит, уже применяли Iterator. Где ещё понадобится итерация? (дерево из Composite — обход по категориям)
Iterator: когда применять / когда не применять; __getitem__ без __iter__ — ошибка
Когда применять
итерация по кастомной структуре: дерево из Composite, курс по студентам, «бесконечные» последовательности.
Когда не применять
стандартные коллекции — for и так работает.
реализуют __getitem__ без __iter__ и удивляются медленной итерации;
путают итерируемый объект и итератор: iter(obj) vs next(obj).
iter(course) возвращает итератор, next(...) двигает его; сам курс — итерируемый, но не итератор.
Что вернёт iter(course) и что делает next(...)? (итератор; двигает его на следующий элемент)
Live-coding
Live-coding: Strategy для комиссий/скидок
Задача:Checkout с комиссиями FixedFee(10), PercentFee(0.05), CombinedFee(10, 0.05); смена стратегии в рантайме checkout.set_strategy(PercentFee(0.02)).
Намеренная ошибка: стратегия с внутренним состоянием (счётчик вызовов) — почему стратегия должна быть чистой функцией?
Повторите за лектором на своих данных: какой результат вернёт CombinedFee(10, 0.05).apply(100)? (15.0 — фиксированная часть + 5%)
Live-coding
Live-coding: Command с undo — история и откат серии
Пишем с нуля:Account, PayCommand, RefundCommand; список history; функции run(cmd) и undo_last(); демонстрируем откат серии из трёх действий.
# заготовка: команды вызываются через историю
history = [] # стек выполненных командdefrun(cmd):
cmd.execute()
history.append(cmd)
defundo_last():
history.pop().undo()
Правило: команды вызываются не напрямую, а через историю — это и есть требование паттерна.
Что будет при повторном undo, когда история уже пуста? (граничный случай — откат невозможен)
Проверь себя
Проверь себя: 4 вопроса перед семинаром
Чем Strategy отличается от Template Method?
Как Observer связан с событийной архитектурой?
Когда нужен Command, а не обычный вызов метода?
Почему в Python Iterator не требует отдельного класса из GoF?
2 минуты письменно, затем сверка с залом; эталоны — в решения-7.md.
Разбор ошибки
Исключение в одном наблюдателе рушит всех — изолируйте обработку
class CourseNotifier:
def__init__(self) -> None:
self._observers: list[Callable[[str], None]] = []
defsubscribe(self, observer: Callable[[str], None]) -> None:
self._observers.append(observer)
defnotify(self, message: str) -> None:
for observer in self._observers:
observer(message) # падение одного рушит всехdefbroken(message: str) -> None:
raiseRuntimeError("сбой в наблюдателе")
notifier = CourseNotifier()
notifier.subscribe(email)
notifier.subscribe(broken)
notifier.subscribe(logger)
notifier.notify("студент записан")
RuntimeError: сбой в наблюдателе
Правило: изолируйте обработку ошибок в цикле уведомления — try/except вокруг каждого наблюдателя, иначе один сбойный подписчик рушит всех.
Найдите баг: где именно падает программа и кто никогда не получит уведомление? (logger — цикл прервался на broken)
Типичные ошибки недели 7
Strategy: стратегия с внутренним состоянием там, где нужна чистая функция; контекст лезет в детали стратегии.
Observer: наблюдатель не отписывается — утечка ссылок и неожиданные вызовы; исключение в одном наблюдателе рушит всех.
Command: команда хранит слишком много контекста; заявлен undo, но не реализован; команды вызываются напрямую, минуя историю.
Template Method vs Strategy: путаница наследования и композиции; слишком много абстрактных шагов.
State: состояния меняют друг друга напрямую, минуя контекст; не проверяются допустимые переходы.
Iterator:__getitem__ без __iter__ — медленная итерация.
Отметьте знакомые ошибки: каждая задача семинара «ловит» одну из них.
Связка с проектом: этап недели 7 «Платёжной системы»
Strategy — комиссии и валюты: фиксированная, процентная, комбинированная;
Command — история с undo, или Observer — события: уведомления, лог.
Пример потока: pay 1000 card → fee 1000 (расчёт комиссии стратегией) → history (команды из стека).
Обоснование выбора — обязательный элемент README проекта: какую боль решает каждый паттерн и почему не другой.
Комиссия меняется в рантайме — какой паттерн? Отмена платежа — какой? (Strategy и Command)
Сегодня вы научились…
устранять ветвления через Strategy;
строить связь «один ко многим» через Observer;
делать действия откладываемыми и отменяемыми через Command;
задавать скелет алгоритма через Template Method;
моделировать конечные автоматы через State;
использовать Iterator как встроенный паттерн.
Всё, что обещали в начале (Слайд 4), — сделали?
Рефлексия
One-minute paper: главное + один вопрос
Напишите (1 минута, не подписывая):
одно самое важное, что вы узнали сегодня;
один вопрос, который остался неясным.
Соберите ответы: 2–3 вопроса преподаватель разбирает сразу или переносит на семинар.
Это сигнал для семинара недели 7: что разобрать подробнее.
Что дальше: семинар недели 7, ДЗ task-07, анонс недели 8
Семинар недели 7 (семинар-7.md): Strategy для скидок, Observer в «Университете», Command с undo, State для заказа, challenge «Strategy против Template Method».
ДЗ task-07 (autograder): Strategy (комиссии) или Command (редактор с undo) — задачи 07–08 банка задач.
Анонс недели 8: идиомы Python и антипаттерны — dataclasses, ABC/Protocol, context manager, класс-гигант, Spaghetti, Copy-Paste, Golden Hammer.
Готовы? Откройте семинар-7.md и выполните базовый уровень.