Современная практика: использовать не по букве, а по духу, адаптируя к языку (Python-специфику покажем сегодня и на неделе 8).
Акцент: паттерн — не алгоритм: алгоритм — последовательность шагов, паттерн — структура и ответственность.
Чем паттерн отличается от готовой библиотеки? (библиотеку подключил — и пользуешься; паттерн — схема, которую реализуешь сам)
Три класса GoF: порождающие / структурные / поведенческие
Порождающие: неделя 5 · Структурные: неделя 6 · Поведенческие: неделя 7 — карта модуля: сегодня левая колонка, далее средняя и правая.
Назовите по одному паттерну каждого класса GoF (вопрос из ТЗ).
Стоп-вопрос
Паттерны — словарь решений и общий язык команды; злоупотребление — когда код стал сложнее
Паттерны — словарь проверенных решений и общий язык команды: фраза «здесь нужен Adapter» мгновенно передаёт структуру решения без пересказа кода.
Критерий уместности: код с паттерном проще менять, чем код без него — иначе паттерн лишний.
Признаки злоупотребления: паттерн применён «для галочки»; код стал сложнее, а не проще; появились классы, которые ничего не решают; нельзя объяснить, какую боль решает паттерн.
Как понять, что паттерн в коде — злоупотребление? (нельзя назвать боль; код стал сложнее; классы ничего не решают)
Боль Factory Method: один и тот же выбор размазан по трём местам
Боль: добавили тип seminar — правим все места; вызывающий код знает конкретные классы; выбор не покрыт тестами.
Три места проекта указывают на один и тот же фрагмент if kind == ...: добавили seminar — правим все три.
Что придётся сделать, чтобы добавить тип seminar? (править все места с if kind == ... — это и есть боль, которую решает фабрика)
CourseFactory через classmethod — единая точка создания
class Course: ...
class Lecture(Course): ...
class Lab(Course): ...
class Practice(Course): ...
class CourseFactory:
@classmethoddefcreate(cls, kind: str, **kwargs) -> Course:
if kind == "lecture":
return Lecture(**kwargs)
if kind == "lab":
return Lab(**kwargs)
if kind == "practice":
return Practice(**kwargs)
raiseValueError(f"Неизвестный тип курса: {kind!r}")
c = CourseFactory.create("lecture", title="АиСД")
print(type(c).__name__)
Lecture
Правило: выбор варианта — в одном месте; вызывающий код не знает конкретный класс; неизвестный тип — ValueError, а не None.
Что вернёт CourseFactory.create("seminar", ...) до добавления ветки? (сейчас — ValueError; после добавления — Seminar без правок вызывающего кода)
Factory Method: когда применять / когда не применять
Когда применять
несколько вариантов объекта;
выбор по данным/конфигу;
логику создания надо централизовать и покрыть тестами.
Когда не применять
один класс без вариантов — фабрика добавляет слой без пользы;
вариант один — достаточно конструктора.
Типичная ошибка: фабрика превращается в функцию-гигант с 20 ветками; return None вместо ValueError (разберём на Слайде 25).
Сколько веток в фабрике — сигнал, что пора пересмотреть выбор? (нет магического числа; признак — фабрика растёт быстрее, чем добавляются реальные типы)
Abstract Factory — согласованные семейства «очно/дистанционно»
Клиент работает только с FormatFactory и всегда получает согласованный набор: очное занятие + бумажный материал ИЛИ онлайн-занятие + видео-материал — семейства не смешиваются.
Почему нельзя просто два раза вызвать Factory Method? (ничто не гарантирует согласованность набора — Abstract Factory гарантирует)
FormatFactory как Protocol: клиент работает с интерфейсом
class Lesson: ...
class Material: ...
class ClassroomLesson(Lesson): ...
class ZoomLesson(Lesson): ...
class PaperMaterial(Material): ...
class VideoMaterial(Material): ...
class FormatFactory(Protocol):
defcreate_lesson(self) -> Lesson: ...
defcreate_material(self) -> Material: ...
Боль: 15-аргументный конструктор нечитаем; легко перепутать места ("Петров" и 30); часть параметров опциональна, часть собирается по шагам; валидация размазана по вызывающему коду.
Что здесь можно перепутать? (места аргументов; читаемость падает, ошибки — на этапе выполнения, валидации нет)
CourseBuilder: пошаговая сборка с валидацией в build()
Правило: сеттеры возвращают self — работают цепочки; __init__ принимает готовые проверенные данные; build() валидирует полноту.
Что вернёт CourseBuilder().build() без названия? (ответ: ValueError: Название курса обязательно)
Builder: когда применять / когда не применять
Когда применять
много опциональных параметров;
несколько «рецептов» сборки одного объекта (курс с расписанием и без; курс с материалами и без).
Когда не применять
3–5 параметров — @dataclass со значениями по умолчанию проще и читаемее;
билдер ради билдера — шум.
Типичная ошибка: билдер без валидации в build() (объект собирается неполным); сеттеры возвращают None вместо self.
Когда Builder избыточен? (3–5 параметров, нет «рецептов» — хватит @dataclass)
Singleton — антипаттерн в Python: модуль — естественный синглтон
Проблема, которую «решает» Singleton: нужен один объект на всё приложение (логгер, конфиг, подключение к БД).
Решение на Python: модуль — естественный синглтон — атрибуты на уровне модуля инициализируются один раз при импорте.
# config.py — синглтон «из коробки»
DEBUG = False
DATABASE_URL = "sqlite:///university.db"# в другом модулеimport config
print(config.DATABASE_URL)
sqlite:///university.db
Акцент: модуль уже решил задачу — класс Singleton через __new__ не нужен.
Когда модуля достаточно? (разделяемое неизменяемое состояние: конфиг, константы, логгер)
Чем вреден Singleton: глобальное мутируемое состояние
Когда Singleton вреден: мутируемое глобальное состояние — тесты начинают зависеть от порядка выполнения; код скрыто связан («кто-то где-то поменял синглтон»).
Пример вреда: два теста меняют один глобальный объект — результат второго зависит от того, выполнился ли первый (продемонстрируем на семинаре).
Правильная альтернатива:явная передача объекта через параметры — предвестник внедрения зависимостей (М3 «Архитектура кода»).
Типичная ошибка: реализуют Singleton через __new__ «потому что паттерн из GoF», не замечая, что модуль уже решил задачу.
Как сделать код тестируемым, если конфиг — глобальный объект, который можно поменять из любого места? (передавать объект явно — предвестник DI, М3)
Предскажите результат
Prototype: b = a — та же ссылка; что изменится в оригинале?
class CoursePlan:
def__init__(self, title: str, lessons: list[str]) -> None:
self.title = title
self.lessons = lessons
a = CoursePlan("АиСД", ["Лекция 1"])
b = a # «копия»?
b.lessons.append("Лекция 2")
print(a.lessons)
Не запуская: что напечатает print(a.lessons)? Присваивание копирует или связывает с тем же объектом?
если копии не должны быть независимыми — разделяемое состояние может быть осознанным.
Чем copy.copy отличается от copy.deepcopy? (поверхностная — вложенные объекты общие; глубокая — рекурсивно копирует)
@dataclass(frozen=True) + classmethod-фабрики заменяют целый порождающий паттерн
from dataclasses import dataclass
@dataclass(frozen=True)
class Student:
name: str
group: str@classmethoddeffrom_str(cls, line: str) -> "Student":
name, group = line.split(",")
return cls(name.strip(), group.strip())
s = Student.from_str("Иван, 101")
print(s)
print(s == Student("Иван", "101"))
Student(name='Иван', group='101')
True
Правило:@dataclass даёт __init__, __repr__, __eq__; frozen=True — неизменяемость; field(default_factory=list) — для изменяемых значений по умолчанию; classmethod-фабрика (from_str) — именованный альтернативный конструктор, в простых случаях заменяющий Factory Method без класса-фабрики.
Когда не применять: ручной __init__/__repr__ там, где хватит dataclass; фабричный класс там, где хватит classmethod.
Почему frozen=True полезен? (неизменяемость: объект не мутируется случайно; можно как ключ в dict/set)
Live-coding
Live-coding: выносим создание курсов в CourseFactory
Задание: на коде проекта «Университет» централизуйте создание курсов разных типов.
Найдите ветвление if kind == ... в своём коде (боль из Слайда 8).
Напишите CourseFactory.create(kind, **kwargs) с ValueError на неизвестный тип.
@dataclass(frozen=True)
class Student:
name: str
group: str@classmethoddeffrom_str(cls, line: str) -> "Student":
name, group = line.split(",")
return cls(name.strip(), group.strip())
Какой порождающий паттерн заменяет from_str? (Factory Method в простой форме)
Разбор ошибки
return None вместо ValueError — ошибка «уезжает» от места вызова
class CourseFactory:
@classmethoddefcreate(cls, kind: str, **kwargs) -> Course | None:
if kind == "lecture":
return Lecture(**kwargs)
returnNone# ошибка: неизвестный тип «проглочен»
course = CourseFactory.create("seminar", title="X")
print(course.title) # AttributeError: 'NoneType' object has no attribute 'title'AttributeError: 'NoneType' object has no attribute 'title'
Разбор: ошибка проявляется далеко от фабрики — в месте обращения к course.title; по traceback непонятно, где создан None. raise ValueError сообщает об ошибке сразу, в точке создания, с понятным сообщением.
Почему raise ValueError лучше, чем return None? (ошибка видна в точке создания, а не «уезжает» к месту использования)
Типичные ошибки недели 5
Паттерн «для галочки»: код стал сложнее, боль не названа.
Функция-гигант — фабрика с 20 ветками; иерархия фабрик там, где хватит одного classmethod.
return None вместо ValueError на неизвестный тип.
Abstract Factory без второго семейства; путаница FM/AF.
Builder без валидации в build(); сеттеры возвращают None вместо self.
Singleton через __new__ с мутируемым состоянием; copy.copy вместо copy.deepcopy; ручной __init__ вместо dataclass; мутируемое значение по умолчанию без default_factory.
Отметьте ошибки, которые вы уже встречали в своём коде «Университета»: каждая задача семинара «ловит» одну из них.
Проверь себя
Проверь себя: 4 вопроса перед семинаром
Что такое паттерн и чем он отличается от готовой библиотеки?
Назовите по одному паттерну каждого из трёх классов GoF.
Как понять, что паттерн в коде — злоупотребление?
Чем Factory Method отличается от Abstract Factory? Почему Singleton в Python — антипаттерн и чем его заменить? Когда Builder уместен, а когда — просто @dataclass? Чем copy.copy отличается от copy.deepcopy?
2 минуты письменно (можно в чат); сверить с залом; эталоны — в решения-5.md.
Связка с проектом: этап недели 5 «Платёжной системы»
Этап недели 5: каркас проекта — модель платежей, PaymentFactory (по образцу CourseFactory), кастомные исключения (PaymentError), git init, первый осмысленный коммит.
class PaymentError(Exception): ...
class Payment:
def__init__(self, amount: float) -> None:
self.amount = amount
class CardPayment(Payment): ...
class WalletPayment(Payment): ...
class PaymentFactory:
@classmethoddefcreate(cls, kind: str, amount: float) -> Payment:
if kind == "card":
return CardPayment(amount)
if kind == "wallet":
return WalletPayment(amount)
raisePaymentError(f"Неизвестный тип платежа: {kind!r}")
Кастомные исключения и фабрика — база, на которую на неделях 6–8 лягут Repository, Strategy и Command.
Какой паттерн здесь? (Factory Method через classmethod — буквально CourseFactory, только для платежей)
Сегодня вы научились…
объяснять паттерны GoF и классифицировать их;
создавать объекты через Factory Method (classmethod) и Abstract Factory (Protocol);
собирать объекты через Builder с валидацией;
объяснять, почему Singleton — антипаттерн, и выбирать модуль/явную передачу;
копировать сложные объекты через copy.deepcopy;
применять @dataclass и classmethod-фабрики.
Всё, что обещали в начале (Слайд 4), — сделали?
Рефлексия
One-minute paper: главное + один вопрос
Напишите (1 минута, не подписывая):
одно самое важное, что вы узнали сегодня;
один вопрос, который остался неясным.
Соберите ответы (в чат/на бумаге); 2–3 вопроса разберём сразу или перенесём на семинар.
Это сигнал для семинара: что разобрать подробнее.
Что дальше: семинар недели 5, ДЗ task-05, анонс недели 6
Семинар недели 5 (семинар-5.md): classmethod-фабрики на dataclass, разбор Singleton, фабрика курсов, Builder, challenge «Abstract Factory очно/дистанционно».
ДЗ task-05 (autograder): фабрика фигур или курсов с тестами.
Анонс недели 6: структурные паттерны — Adapter, Facade, Proxy, Decorator, Composite («как компоновать классы, не ломая интерфейсы»).
Готовы? Откройте семинар-5.md и выполните базовый уровень.