ООАП. Лекция 7 — Паттерны поведения¶
Конспект лектора. Источник — «Приёмы объектно-ориентированного проектирования» (GoF): назначение, применимость, участники — по книге. Код — C#, диаграммы — Mermaid. Паттернов десять, поэтому темп плотный: на каждый — задача, структура, код, плата.
Блок 1. Что общего у паттернов поведения¶
Паттерны поведения связаны с алгоритмами и распределением обязанностей между объектами. Они описывают не столько структуру классов, сколько схемы связей и потоки запросов.
Как и раньше, есть уровень класса (наследование: шаблонный метод, интерпретатор) и уровень объекта (композиция и делегирование: все остальные).
Что варьирует каждый — по таблице переменных аспектов из книги:
| Паттерн | Что можно менять, не трогая клиента |
|---|---|
| Стратегия | алгоритм |
| Шаблонный метод | шаги алгоритма |
| Наблюдатель | множество зависящих объектов и способ их обновления |
| Команда | время и способ выполнения запроса |
| Состояние | состояние объекта |
| Цепочка обязанностей | объект, выполняющий запрос |
| Итератор | способ перебора элементов агрегата |
| Посредник | взаимодействующие объекты и механизм их совместной работы |
| Хранитель | закрытая информация вне объекта и время её сохранения |
| Посетитель | операции над объектами без изменения их классов |
Общая нить: инкапсулировать то, что меняется, и заменить жёсткие связи косвенностью.
Блок 2. Strategy и Template Method¶
Разбираем парой: они решают одну задачу — «варьировать алгоритм» — но один делает это композицией, другой наследованием.
Strategy — стратегия¶
Назначение. Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет изменять алгоритм независимо от клиентов, которые им пользуются.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Composition {
-ICompositor compositor
+Repair() void
}
class ICompositor {
<<interface>>
+Compose(Coord[] natural, int lineWidth) int
}
class SimpleCompositor
class TeXCompositor
class ArrayCompositor
Composition o-- ICompositor : стратегия
ICompositor <|.. SimpleCompositor
ICompositor <|.. TeXCompositor
ICompositor <|.. ArrayCompositor
Исходник диаграммы
classDiagram
direction LR
class Composition {
-ICompositor compositor
+Repair() void
}
class ICompositor {
<<interface>>
+Compose(Coord[] natural, int lineWidth) int
}
class SimpleCompositor
class TeXCompositor
class ArrayCompositor
Composition o-- ICompositor : стратегия
ICompositor <|.. SimpleCompositor
ICompositor <|.. TeXCompositor
ICompositor <|.. ArrayCompositor
public interface IShippingCost { Money Calculate(Order order); }
public sealed class FlatRate : IShippingCost
{
public Money Calculate(Order order) => Money.Of(5, "EUR");
}
public sealed class ByWeight : IShippingCost
{
public Money Calculate(Order order) => Money.Of(order.Weight * 0.8m, "EUR");
}
public sealed class Checkout // контекст
{
private readonly IShippingCost _shipping;
public Checkout(IShippingCost shipping) => _shipping = shipping; // алгоритм извне
public Money Total(Order order) => order.Total() + _shipping.Calculate(order);
}
from typing import Callable
ShippingCost = Callable[[Order], Money] # стратегия — просто вызываемое
def flat_rate(order: Order) -> Money: return Money(5, "EUR")
def by_weight(order: Order) -> Money: return Money(order.weight * 0.8, "EUR")
class Checkout:
def __init__(self, shipping: ShippingCost) -> None:
self._shipping = shipping # функция вместо объекта-стратегии
def total(self, order: Order) -> Money:
return order.total() + self._shipping(order)
Применимость. Много родственных классов отличаются только поведением; нужны разные варианты алгоритма; алгоритм использует данные, о которых клиенту знать не нужно; в классе много условных операторов, выбирающих поведение — их место в отдельных стратегиях.
Результаты. Семейство алгоритмов, альтернатива наследованию, отказ от условных операторов. ⚠️ Клиент должен знать о различиях стратегий, чтобы выбрать нужную; растёт число объектов; накладные расходы на обмен данными между контекстом и стратегией.
В C#. Часто стратегия схлопывается в делегат:
Func<Order, Money>вместо интерфейса с одним методом. Это законно и короче — но теряется имя типа, а вместе с ним и удобство регистрации в DI. Правило: одна операция без состояния — делегат; несколько связанных операций или нужна конфигурация — интерфейс.
Template Method — шаблонный метод¶
Назначение. Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет переопределять шаги, не меняя структуру алгоритма.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
class ReportGenerator {
<<abstract>>
+Generate() Report
#LoadData()* DataSet
#Format(DataSet d)* string
#OnFinished() void
}
class SalesReport
class StockReport
ReportGenerator <|-- SalesReport
ReportGenerator <|-- StockReport
note for ReportGenerator "Generate() — шаблонный метод:<br/>порядок шагов фиксирован"
Исходник диаграммы
classDiagram
class ReportGenerator {
<<abstract>>
+Generate() Report
#LoadData()* DataSet
#Format(DataSet d)* string
#OnFinished() void
}
class SalesReport
class StockReport
ReportGenerator <|-- SalesReport
ReportGenerator <|-- StockReport
note for ReportGenerator "Generate() — шаблонный метод:<br/>порядок шагов фиксирован"
public abstract class ReportGenerator
{
public Report Generate() // шаблонный метод: не виртуальный
{
var data = LoadData(); // обязательный шаг подкласса
var body = Format(data);
OnFinished(); // hook: по умолчанию ничего
return new Report(body);
}
protected abstract DataSet LoadData();
protected abstract string Format(DataSet data);
protected virtual void OnFinished() { } // операция-зацепка
}
from abc import ABC, abstractmethod
class ReportGenerator(ABC):
def generate(self) -> Report: # шаблонный метод
data = self.load_data()
body = self.format(data)
self.on_finished()
return Report(body)
@abstractmethod
def load_data(self) -> DataSet: ...
@abstractmethod
def format(self, data: DataSet) -> str: ...
def on_finished(self) -> None: ... # зацепка с пустой реализацией
Ключевая идея — «голливудский принцип»: не звоните нам, мы позвоним вам. Базовый класс зовёт методы подкласса, а не наоборот. Именно так устроены каркасы (лекция 1).
Стратегия против шаблонного метода. Один вопрос: когда фиксируется выбор. Наследование — на этапе компиляции, один вариант на класс; композиция — в рантайме, можно менять и комбинировать. Плюс шаблонный метод позволяет переиспользовать структуру алгоритма, а стратегия — заменить его целиком.
Блок 3. Observer — наблюдатель¶
Назначение. Определяет зависимость типа «один-ко-многим» между объектами так, что при изменении состояния одного объекта все зависящие от него получают уведомление и автоматически обновляются.
Задача. Пример книги — таблица, столбчатая и круговая диаграммы поверх одних данных: изменили данные — перерисовались все представления, при этом данные о представлениях ничего не знают.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class ISubject {
<<interface>>
+Attach(IObserver o) void
+Detach(IObserver o) void
+Notify() void
}
class IObserver {
<<interface>>
+Update(ISubject subject) void
}
class SalesData {
-List~IObserver~ observers
+Add(Sale s) void
+GetState() Report
}
class BarChart
class PieChart
ISubject <|.. SalesData
IObserver <|.. BarChart
IObserver <|.. PieChart
SalesData o-- IObserver : наблюдатели
BarChart --> SalesData : запрашивает состояние
Исходник диаграммы
classDiagram
direction LR
class ISubject {
<<interface>>
+Attach(IObserver o) void
+Detach(IObserver o) void
+Notify() void
}
class IObserver {
<<interface>>
+Update(ISubject subject) void
}
class SalesData {
-List~IObserver~ observers
+Add(Sale s) void
+GetState() Report
}
class BarChart
class PieChart
ISubject <|.. SalesData
IObserver <|.. BarChart
IObserver <|.. PieChart
SalesData o-- IObserver : наблюдатели
BarChart --> SalesData : запрашивает состояние
public sealed class SalesData // субъект
{
private readonly List<IObserver> _observers = new();
private readonly List<Sale> _sales = new();
public void Attach(IObserver o) => _observers.Add(o);
public void Detach(IObserver o) => _observers.Remove(o);
public void Add(Sale sale)
{
_sales.Add(sale);
Notify(); // изменение состояния → уведомление
}
private void Notify()
{
foreach (var o in _observers.ToArray()) o.Update(this); // копия: список могут менять в Update
}
}
class SalesData:
def __init__(self) -> None:
self._observers: list[Callable[["SalesData"], None]] = []
self._sales: list[Sale] = []
def attach(self, observer) -> None: self._observers.append(observer)
def detach(self, observer) -> None: self._observers.remove(observer)
def add(self, sale: Sale) -> None:
self._sales.append(sale)
for observer in list(self._observers): # копия: подписчики могут отписаться
observer(self) # наблюдатель — любое вызываемое
Две модели передачи данных (по книге).
- Проталкивание (push) — субъект шлёт подробности изменения в
Update. Быстрее, но субъект делает предположения о том, что нужно наблюдателям. - Вытягивание (pull) — субъект шлёт только «я изменился», наблюдатель сам запрашивает состояние. Гибче, но может потребовать нескольких обращений.
Результаты. Абстрактная связанность субъекта и наблюдателя, широковещательные
уведомления. ⚠️ Опасность: неожиданные каскадные обновления. Наблюдатели не знают друг
о друге, поэтому одно изменение может вызвать лавину обновлений, а цепочку трудно
проследить. Плюс классическая утечка: забыли Detach — объект живёт вечно.
В .NET. События (
event),IObservable<T>/IObserver<T>и Rx,INotifyPropertyChangedиз MVVM (лекция 2) — всё это наблюдатель. Слабые события (WeakEventManager) существуют ровно из-за проблемы сDetach.
Блок 4. Command — команда¶
Назначение. Инкапсулирует запрос в виде объекта, позволяя параметризовать клиентов типом запроса, ставить запросы в очередь, протоколировать их и поддерживать отмену операций.
Задача. Пример книги — меню и кнопки редактора: пункт меню не должен знать, что именно
он делает. Он знает объект-команду, у которой есть Execute.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction TB
class ICommand {
<<interface>>
+Execute() void
+Unexecute() void
}
class PasteCommand {
-Document document
+Execute() void
+Unexecute() void
}
class OpenCommand
class MenuItem {
-ICommand command
+Clicked() void
}
class History {
-Stack~ICommand~ done
+Push(ICommand c) void
+Undo() void
}
ICommand <|.. PasteCommand
ICommand <|.. OpenCommand
MenuItem o-- ICommand
History o-- ICommand : журнал для отмены
Исходник диаграммы
classDiagram
direction TB
class ICommand {
<<interface>>
+Execute() void
+Unexecute() void
}
class PasteCommand {
-Document document
+Execute() void
+Unexecute() void
}
class OpenCommand
class MenuItem {
-ICommand command
+Clicked() void
}
class History {
-Stack~ICommand~ done
+Push(ICommand c) void
+Undo() void
}
ICommand <|.. PasteCommand
ICommand <|.. OpenCommand
MenuItem o-- ICommand
History o-- ICommand : журнал для отмены
public interface ICommand
{
void Execute();
void Unexecute();
}
public sealed class PasteCommand : ICommand
{
private readonly Document _document;
private readonly string _text;
private int _position;
public PasteCommand(Document document, string text) => (_document, _text) = (document, text);
public void Execute()
{
_position = _document.CaretPosition; // запоминаем для отмены
_document.Insert(_position, _text);
}
public void Unexecute() => _document.Delete(_position, _text.Length);
}
public sealed class History // журнал команд
{
private readonly Stack<ICommand> _done = new();
public void Run(ICommand c) { c.Execute(); _done.Push(c); }
public void Undo() { if (_done.Count > 0) _done.Pop().Unexecute(); }
}
from dataclasses import dataclass, field
@dataclass
class PasteCommand: # протокол: execute / unexecute
document: Document
text: str
position: int = field(default=0, init=False)
def execute(self) -> None:
self.position = self.document.caret_position
self.document.insert(self.position, self.text)
def unexecute(self) -> None:
self.document.delete(self.position, len(self.text))
class History:
def __init__(self) -> None: self._done: list = []
def run(self, command) -> None: command.execute(); self._done.append(command)
def undo(self) -> None:
if self._done: self._done.pop().unexecute()
Применимость. Параметризовать объекты выполняемым действием; определять, ставить в очередь и выполнять запросы в разное время; поддерживать отмену; протоколировать изменения для восстановления после сбоя; строить систему на высокоуровневых операциях из примитивных (транзакции).
Результаты. Отделяет инициатора от исполнителя, делает команды объектами первого класса (их можно хранить, передавать, комбинировать в макрокоманды через компоновщик). ⚠️ Число классов растёт; для отмены нужно хранить состояние, а для многоуровневой отмены — следить, чтобы команды не накапливали ошибок при повторном выполнении.
В .NET.
ICommandв MVVM — этот же паттерн, встроенный в платформу. MediatR, очереди сообщений,Action-делегаты как простейшие команды. Отмена в редакторах, миграции БД (Up/Down) — команда с обратной операцией.
Блок 5. State — состояние¶
Назначение. Позволяет объекту изменять поведение при изменении внутреннего состояния. Со стороны выглядит так, будто объект поменял свой класс.
Задача. Пример книги — TCP-соединение: Open, Close, Acknowledge работают
по-разному в состояниях Established, Listen, Closed. Без паттерна это гигантские switch
в каждом методе.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class TcpConnection {
-TcpState state
+Open() void
+Close() void
+Acknowledge() void
}
class TcpState {
<<abstract>>
+Open(TcpConnection c) void
+Close(TcpConnection c) void
}
class TcpEstablished
class TcpListen
class TcpClosed
TcpConnection o-- TcpState : текущее состояние
TcpState <|-- TcpEstablished
TcpState <|-- TcpListen
TcpState <|-- TcpClosed
Исходник диаграммы
classDiagram
direction LR
class TcpConnection {
-TcpState state
+Open() void
+Close() void
+Acknowledge() void
}
class TcpState {
<<abstract>>
+Open(TcpConnection c) void
+Close(TcpConnection c) void
}
class TcpEstablished
class TcpListen
class TcpClosed
TcpConnection o-- TcpState : текущее состояние
TcpState <|-- TcpEstablished
TcpState <|-- TcpListen
TcpState <|-- TcpClosed
public sealed class TcpConnection
{
private TcpState _state = new TcpClosed();
internal void ChangeState(TcpState next) => _state = next;
public void Open() => _state.Open(this); // делегирование состоянию
public void Close() => _state.Close(this);
}
public abstract class TcpState
{
public virtual void Open(TcpConnection c) => throw new InvalidOperationException();
public virtual void Close(TcpConnection c) => throw new InvalidOperationException();
}
public sealed class TcpClosed : TcpState
{
public override void Open(TcpConnection c) => c.ChangeState(new TcpEstablished());
}
Результаты. Локализует поведение состояния в одном классе; делает переходы явными (состояние меняется присваиванием объекта, а не набором флагов); объекты состояний можно разделять, если у них нет собственных данных. ⚠️ Растёт число классов; надо решить, кто определяет переходы — контекст или сами состояния (второе гибче, но состояния начинают знать друг о друге).
Состояние против стратегии. Диаграммы одинаковые, намерение разное: стратегию выбирает клиент и обычно один раз, состояние меняется само по ходу жизни объекта. Связь с лекцией 4 прямая: диаграмма состояний — это проект, паттерн State — одна из трёх её реализаций.
Блок 6. Chain of Responsibility — цепочка обязанностей¶
Назначение. Позволяет избежать жёсткой привязки отправителя запроса к получателю, давая шанс обработать запрос нескольким объектам. Получатели связываются в цепочку, и запрос идёт по ней, пока кто-нибудь его не обработает.
Задача. Пример книги — контекстная справка: кнопка не знает справки, передаёт запрос диалогу, тот — приложению. Обработает тот, у кого есть ответ.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Handler {
<<abstract>>
#Handler successor
+HandleRequest(Request r) void
}
class Button
class Dialog
class Application
Handler <|-- Button
Handler <|-- Dialog
Handler <|-- Application
Handler o-- Handler : преемник
Исходник диаграммы
public abstract class Handler
{
private readonly Handler? _successor;
protected Handler(Handler? successor = null) => _successor = successor;
public virtual void Handle(Request request) => _successor?.Handle(request);
// если не обработал — передал дальше; в конце цепочки запрос может остаться без ответа
}
public sealed class ValidationHandler : Handler
{
public ValidationHandler(Handler? next) : base(next) { }
public override void Handle(Request request)
{
if (!request.IsValid) { request.Reject("Некорректные данные"); return; }
base.Handle(request);
}
}
Результаты. Ослабляет связанность: ни отправитель, ни получатели не знают друг о друге; цепочку можно менять в рантайме. ⚠️ Получение не гарантировано — запрос может дойти до конца цепочки и остаться без обработки, и это надо предусматривать явно.
В .NET. Конвейер middleware в ASP.NET Core — цепочка обязанностей в чистом виде;
DelegatingHandlerвHttpClient; обработка исключений по стеку вызовов — та же идея на уровне языка.
Блок 7. Iterator — итератор¶
Назначение. Даёт способ последовательно обойти элементы составного объекта, не раскрывая его внутреннего представления.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class IAggregate~T~ {
<<interface>>
+CreateIterator() IIterator~T~
}
class IIterator~T~ {
<<interface>>
+First() void
+Next() void
+IsDone() bool
+CurrentItem() T
}
class ListAggregate~T~
class ListIterator~T~
IAggregate~T~ <|.. ListAggregate~T~
IIterator~T~ <|.. ListIterator~T~
ListAggregate~T~ ..> ListIterator~T~ : создаёт
Исходник диаграммы
classDiagram
direction LR
class IAggregate~T~ {
<<interface>>
+CreateIterator() IIterator~T~
}
class IIterator~T~ {
<<interface>>
+First() void
+Next() void
+IsDone() bool
+CurrentItem() T
}
class ListAggregate~T~
class ListIterator~T~
IAggregate~T~ <|.. ListAggregate~T~
IIterator~T~ <|.. ListIterator~T~
ListAggregate~T~ ..> ListIterator~T~ : создаёт
public sealed class BinaryTree<T> : IEnumerable<T>
{
private Node<T>? _root;
public IEnumerator<T> GetEnumerator() => InOrder(_root).GetEnumerator();
private static IEnumerable<T> InOrder(Node<T>? node) // внутреннее устройство скрыто
{
if (node is null) yield break;
foreach (var left in InOrder(node.Left)) yield return left;
yield return node.Value;
foreach (var right in InOrder(node.Right)) yield return right;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
class BinaryTree:
def __init__(self, root: Node | None = None) -> None:
self._root = root
def __iter__(self): # протокол итерирования языка
yield from self._in_order(self._root)
@staticmethod
def _in_order(node: Node | None):
if node is None: return
yield from BinaryTree._in_order(node.left)
yield node.value # генератор = внешний итератор бесплатно
yield from BinaryTree._in_order(node.right)
Важное различение из книги. Итератор бывает внешний (клиент управляет обходом:
MoveNext) и внутренний (агрегат сам обходит, клиент передаёт операцию — ForEach).
Внешний гибче: можно сравнивать две коллекции, прерывать обход.
Результаты. Поддерживает разные способы обхода одной коллекции, упрощает интерфейс
агрегата, позволяет вести несколько обходов одновременно. ⚠️ Хрупкость при изменении
коллекции во время обхода — отсюда InvalidOperationException в .NET.
В .NET.
IEnumerable<T>/IEnumerator<T>иyield return— паттерн, встроенный в язык: компилятор генерирует машину состояний, а вы пишете обход как обычный код. В Python — протокол__iter__/__next__и генераторы; в Rust — трейтIterator. Это редкий случай, когда паттерн полностью растворился в языке.
Блок 8. Mediator — посредник¶
Назначение. Определяет объект, инкапсулирующий способ взаимодействия множества объектов. Обеспечивает слабую связанность, избавляя объекты от необходимости явно ссылаться друг на друга, и позволяет независимо менять схему их взаимодействия.
Задача. Пример книги — диалоговое окно: список, кнопка и поле ввода связаны правилами (выбрали в списке — заполнилось поле, поле пустое — кнопка неактивна). Если каждый виджет знает про остальных, диалог невозможно переиспользовать.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction TB
class DialogDirector {
<<abstract>>
+WidgetChanged(Widget w) void
}
class FontDialogDirector {
-ListBox list
-EntryField field
+WidgetChanged(Widget w) void
}
class Widget {
<<abstract>>
#DialogDirector director
+Changed() void
}
class ListBox
class EntryField
DialogDirector <|-- FontDialogDirector
Widget <|-- ListBox
Widget <|-- EntryField
Widget o-- DialogDirector
FontDialogDirector --> ListBox
FontDialogDirector --> EntryField
Исходник диаграммы
classDiagram
direction TB
class DialogDirector {
<<abstract>>
+WidgetChanged(Widget w) void
}
class FontDialogDirector {
-ListBox list
-EntryField field
+WidgetChanged(Widget w) void
}
class Widget {
<<abstract>>
#DialogDirector director
+Changed() void
}
class ListBox
class EntryField
DialogDirector <|-- FontDialogDirector
Widget <|-- ListBox
Widget <|-- EntryField
Widget o-- DialogDirector
FontDialogDirector --> ListBox
FontDialogDirector --> EntryField
public sealed class FontDialogDirector
{
private readonly ListBox _fonts;
private readonly EntryField _field;
public void WidgetChanged(Widget source) // все правила связи — здесь
{
if (source == _fonts) _field.SetText(_fonts.Selected);
else if (source == _field) _fonts.Select(_field.Text);
}
}
Результаты. Связи «многие ко многим» превращаются в «один ко многим»: вместо паутины — звезда. Взаимодействие становится отдельной сущностью, которую можно изучать и менять. ⚠️ Посредник легко превращается в God Object: вся сложность переезжает в него. Это осознанный размен — сложность связей на сложность одного класса.
Посредник против фасада. Фасад однонаправлен: клиенты зовут подсистему, подсистема о фасаде не знает. Посредник двунаправлен: коллеги знают о посреднике и общаются через него.
Блок 9. Memento — хранитель¶
Назначение. Не нарушая инкапсуляции, получает и сохраняет во внешней памяти внутреннее состояние объекта, чтобы позже объект можно было восстановить в точно таком же состоянии.
Задача. Отмена операции требует запомнить состояние. Но выставить наружу все поля — значит сломать инкапсуляцию, ради которой всё и затевалось.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Originator {
+CreateMemento() Memento
+SetMemento(Memento m) void
}
class Memento {
-State state
+GetState() State
}
class Caretaker {
-Memento memento
}
Originator ..> Memento : создаёт
Caretaker o-- Memento : хранит, но не заглядывает
Исходник диаграммы
Ключевая идея — два интерфейса. Широкий, доступный только создателю (Originator),
и узкий, доступный смотрителю (Caretaker): смотритель может хранить и передавать снимок,
но не может его прочитать или изменить.
public sealed class Editor // Originator
{
private string _text = "";
private int _caret;
public sealed class Snapshot // вложенный тип: доступ к приватным полям
{
private readonly string _text;
private readonly int _caret;
internal Snapshot(string text, int caret) => (_text, _caret) = (text, caret);
internal void Restore(Editor editor) => (editor._text, editor._caret) = (_text, _caret);
}
public Snapshot Save() => new(_text, _caret);
public void Restore(Snapshot snapshot) => snapshot.Restore(this);
}
// Caretaker хранит Snapshot и ничего не знает о его содержимом
var history = new Stack<Editor.Snapshot>();
Результаты. Сохраняет инкапсуляцию и упрощает создателя (ему не нужно вести журнал версий). ⚠️ Хранители могут дорого обходиться по памяти; смотрителю неизвестна стоимость хранения; в языках без модификаторов «только для друзей» узкий интерфейс приходится обеспечивать соглашениями.
Комбинация. Хранитель почти всегда идёт с командой: команда хранит снимок для
Unexecute. В .NET есть и «дешёвый» вариант: неизменяемые записи — снимок получаетсяwith-выражением, а старая версия остаётся валидной сама по себе.
Блок 10. Visitor — посетитель¶
Назначение. Представляет операцию, выполняемую над элементами структуры объектов. Позволяет определить новую операцию, не изменяя классы этих элементов.
Задача. Пример книги — компилятор: по дереву разбора надо выполнять проверку типов, оптимизацию, генерацию кода, красивую печать. Складывать все эти операции в классы узлов — значит превращать их в свалку.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction TB
class INodeVisitor {
<<interface>>
+VisitAssignment(AssignmentNode n) void
+VisitVariable(VariableNode n) void
}
class TypeCheckingVisitor
class CodeGenVisitor
class Node {
<<abstract>>
+Accept(INodeVisitor v)* void
}
class AssignmentNode {
+Accept(INodeVisitor v) void
}
class VariableNode {
+Accept(INodeVisitor v) void
}
INodeVisitor <|.. TypeCheckingVisitor
INodeVisitor <|.. CodeGenVisitor
Node <|-- AssignmentNode
Node <|-- VariableNode
Node ..> INodeVisitor : Accept
Исходник диаграммы
classDiagram
direction TB
class INodeVisitor {
<<interface>>
+VisitAssignment(AssignmentNode n) void
+VisitVariable(VariableNode n) void
}
class TypeCheckingVisitor
class CodeGenVisitor
class Node {
<<abstract>>
+Accept(INodeVisitor v)* void
}
class AssignmentNode {
+Accept(INodeVisitor v) void
}
class VariableNode {
+Accept(INodeVisitor v) void
}
INodeVisitor <|.. TypeCheckingVisitor
INodeVisitor <|.. CodeGenVisitor
Node <|-- AssignmentNode
Node <|-- VariableNode
Node ..> INodeVisitor : Accept
public interface INodeVisitor
{
void Visit(AssignmentNode node);
void Visit(VariableNode node);
}
public abstract class Node
{
public abstract void Accept(INodeVisitor visitor); // двойная диспетчеризация
}
public sealed class VariableNode : Node
{
public string Name { get; init; } = "";
public override void Accept(INodeVisitor visitor) => visitor.Visit(this);
}
public sealed class TypeCheckingVisitor : INodeVisitor // новая операция без правки узлов
{
public void Visit(AssignmentNode node) { }
public void Visit(VariableNode node) { }
}
from functools import singledispatch
@singledispatch # диспетчеризация по типу первого аргумента
def type_check(node: Node) -> None:
raise NotImplementedError(type(node).__name__)
@type_check.register
def _(node: VariableNode) -> None: ...
@type_check.register
def _(node: AssignmentNode) -> None:
type_check(node.target); type_check(node.value)
# Accept писать не нужно: одиночная диспетчеризация по типу даётся языком.
# Плата та же, что у switch в C#: забытый узел обнаружится только в рантайме.
Двойная диспетчеризация — механизм, на котором стоит паттерн: выполняемая операция
зависит от двух типов — типа посетителя и типа узла. Accept выбирает узел,
Visit — посетителя.
Применимость. В структуре есть объекты многих классов с различными интерфейсами, и нужно выполнять над ними операции, зависящие от конкретных классов; над объектами структуры выполняется много несвязанных операций, и не хочется засорять ими классы; классы структуры редко меняются, а операции добавляются часто.
Результаты. Добавить операцию просто — это новый класс посетителя; родственные операции собраны вместе, а не размазаны по узлам; посетитель может накапливать состояние при обходе. ⚠️ Главная плата, о которой книга предупреждает прямо: добавить новый класс элемента трудно — придётся править интерфейс посетителя и все его реализации. Плюс посетителю часто нужен доступ к внутренностям элементов, что ослабляет инкапсуляцию.
Правило выбора: много операций и стабильная иерархия — посетитель; стабильные операции и растущая иерархия — обычные виртуальные методы. Эта дилемма известна как «проблема выражения».
В C#. Сопоставление с образцом по типу (
obj switch { VariableNode v => ..., ... }) решает ту же задачу безAccept, но теряет проверку полноты — компилятор не скажет, что вы забыли узел. В F# и Rust размеченные объединения дают и то и другое: операции добавляются свободно, а исчерпывающность проверяется компилятором. Подробнее посетитель разбирается в материале для самостоятельной работы.
Итоги, ошибки и домашнее задание¶
Пары, которые путают¶
| Пара | Чем различаются |
|---|---|
| Стратегия / Состояние | стратегию выбирает клиент; состояние меняет себя само по ходу жизни объекта |
| Стратегия / Шаблонный метод | композиция и рантайм против наследования и компиляции |
| Наблюдатель / Посредник | наблюдатель — рассылка «один ко многим»; посредник — центр связей «многие ко многим» |
| Посредник / Фасад | посредник двунаправлен, коллеги о нём знают; фасад однонаправлен |
| Команда / Стратегия | команда инкапсулирует запрос (что сделать и когда), стратегия — алгоритм (как сделать) |
| Хранитель / Прототип | хранитель сохраняет состояние ради восстановления, прототип копирует ради создания |
| Посетитель / виртуальные методы | посетитель — когда операции растут, а иерархия стабильна; наоборот — обычный полиморфизм |
Частые ошибки¶
- Наблюдатель без отписки — утечка памяти и «мёртвые» подписчики.
- Каскад уведомлений — обновление тянет обновление, и цепочку не отследить.
- Посредник-God Object — сложность связей заменили сложностью одного класса и на этом остановились.
- Цепочка без гаранта — никто не обработал запрос, а клиент считает, что всё хорошо.
- Состояние, реализованное флагами — четыре булевых поля вместо явных переходов (см. лекцию 4).
- Посетитель поверх растущей иерархии — каждый новый узел ломает всех посетителей.
- Команда без обратной операции, названная командой ради названия.
Домашнее задание¶
- Взять место в своём проекте, где ветвление выбирает поведение, и реализовать его двумя способами: стратегией и шаблонным методом. В отчёте — сравнение и вывод, какой вариант здесь уместнее и почему.
- Реализовать отмену операций через команду с журналом (минимум три вида команд, включая макрокоманду через компоновщик) и хранитель для одного из состояний.
- Реализовать наблюдателя двумя способами — через события .NET и через явные
Attach/Detach— и показать, где возникает утечка при отсутствии отписки. - Обойти древовидную структуру из ДЗ по лекции 6 двумя посетителями (например, подсчёт статистики и экспорт в другой формат). Затем переписать то же самое на сопоставлении с образцом и сравнить, что произойдёт при добавлении нового вида узла.
- Диаграммы классов и последовательности — в Mermaid, имена участников как в книге (Context, Strategy, Subject, Observer, Invoker, Receiver, Originator, Caretaker, Element, Visitor).