ООАП. Лекция 6 — Структурные паттерны¶
Конспект лектора. Источник — «Приёмы объектно-ориентированного проектирования» (GoF): назначение, применимость и участники даются по книге. Код — C#, диаграммы — Mermaid.
Блок 1. Что общего у структурных паттернов¶
Структурные паттерны отвечают на вопрос, как из классов и объектов составлять более крупные структуры.
Разделение на два уровня здесь особенно наглядно:
- паттерны уровня класса используют наследование, чтобы объединить интерфейсы или реализации. Такая структура фиксируется на этапе компиляции. Из каталога это классовый вариант адаптера.
- паттерны уровня объекта компонуют объекты. Структура складывается во время выполнения, и потому её можно менять — это даёт гибкость, недостижимую статическим наследованием.
Что варьирует каждый (по таблице переменных аспектов из лекции 5):
| Паттерн | Что можно менять, не трогая клиента |
|---|---|
| Адаптер | интерфейс к объекту |
| Мост | реализацию объекта |
| Компоновщик | структуру и состав объекта |
| Декоратор | обязанности объекта без порождения подкласса |
| Фасад | интерфейс к подсистеме |
| Приспособленец | затраты на хранение объектов |
| Заместитель | способ доступа к объекту и его местоположение |
Держите эту таблицу перед глазами всю лекцию: три из семи паттернов имеют почти одинаковые диаграммы, и различаются они именно намерением.
Блок 2. Adapter — адаптер¶
Назначение. Преобразует интерфейс класса в другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без него из-за несовместимости интерфейсов.
Задача. Есть готовый класс с нужным поведением, но не тем интерфейсом: библиотека, легаси-код, чужой SDK. Менять его нельзя.
GoF описывает две схемы адаптера — редкий случай, когда один паттерн существует и на уровне класса, и на уровне объекта:
- адаптер объектов — адаптер реализует
Targetи хранит ссылку наAdaptee, переадресуя ему вызовы (композиция); - адаптер классов — адаптер наследует сразу и
Target, иAdaptee(множественное наследование).
Адаптер объектов¶
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Client
class ITarget {
<<interface>>
+Request() void
}
class Adaptee {
+SpecificRequest() void
}
class ObjectAdapter {
-Adaptee adaptee
+Request() void
}
Client --> ITarget
ITarget <|.. ObjectAdapter
ObjectAdapter o-- Adaptee : хранит ссылку
note for ObjectAdapter "Request() вызывает<br/>adaptee.SpecificRequest()"
Исходник диаграммы
classDiagram
direction LR
class Client
class ITarget {
<<interface>>
+Request() void
}
class Adaptee {
+SpecificRequest() void
}
class ObjectAdapter {
-Adaptee adaptee
+Request() void
}
Client --> ITarget
ITarget <|.. ObjectAdapter
ObjectAdapter o-- Adaptee : хранит ссылку
note for ObjectAdapter "Request() вызывает<br/>adaptee.SpecificRequest()"
// Adaptee: чужой класс, менять нельзя
public sealed class LegacyPrinter
{
public void PrintText(string text, int copies) { }
}
// Target: интерфейс, который ждёт наш код
public interface IDocumentPrinter { void Print(Document doc); }
// Адаптер объектов: композиция + перевод вызовов и данных
public sealed class LegacyPrinterAdapter : IDocumentPrinter
{
private readonly LegacyPrinter _printer;
public LegacyPrinterAdapter(LegacyPrinter printer) => _printer = printer;
public void Print(Document doc) => _printer.PrintText(doc.ToPlainText(), copies: 1);
}
Применимость. Хотите использовать существующий класс, но его интерфейс не подходит; нужно создать повторно используемый класс, работающий с заранее неизвестными классами.
Результаты. Адаптер объектов работает с самим Adaptee и всеми его подклассами,
но переопределить поведение Adaptee труднее — оно спрятано за ссылкой. Адаптер классов,
наоборот, позволяет переопределять поведение Adaptee, но адаптирует ровно один класс
и с его подклассами не работает.
Адаптер классов¶
⚠️ Из двух схем в C# реализуется только адаптер объектов. Адаптер классов наследует
одновременно Target и Adaptee, то есть требует множественного наследования классов,
которого в C# нет. Смотрим вторую схему на Python — там она пишется ровно так, как
нарисована у GoF:
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Client
class Target {
+Request() void
}
class Adaptee {
+SpecificRequest() void
}
class ClassAdapter {
+Request() void
}
Client --> Target
Target <|-- ClassAdapter : наследует интерфейс
Adaptee <|-- ClassAdapter : наследует реализацию
note for ClassAdapter "Request() вызывает<br/>унаследованный SpecificRequest()"
Исходник диаграммы
classDiagram
direction LR
class Client
class Target {
+Request() void
}
class Adaptee {
+SpecificRequest() void
}
class ClassAdapter {
+Request() void
}
Client --> Target
Target <|-- ClassAdapter : наследует интерфейс
Adaptee <|-- ClassAdapter : наследует реализацию
note for ClassAdapter "Request() вызывает<br/>унаследованный SpecificRequest()"
class LegacyPrinter: # Adaptee
def print_text(self, text: str, copies: int) -> None: ...
class DocumentPrinter: # Target
def print(self, doc: Document) -> None: raise NotImplementedError
class LegacyPrinterAdapter(DocumentPrinter, LegacyPrinter): # наследуем оба
def print(self, doc: Document) -> None:
self.print_text(doc.to_plain_text(), copies=1) # метод достался по наследству
Разницу стоит проговорить вслух: в версии на C# адаптер вызывает чужой объект, в версии на Python — является им и вызывает унаследованный метод. Отсюда и разные последствия из абзаца выше.
В .NET.
StreamReaderповерхStream,DataAdapter, обёртки над native-API — сплошные адаптеры объектов. В Python адаптер часто не нужен вовсе: утиная типизация позволяет подсунуть объект с нужными методами. В Go тот же эффект даёт неявная реализация интерфейсов.
Блок 3. Bridge — мост¶
Назначение. Отделяет абстракцию от её реализации, чтобы то и другое можно было изменять независимо.
Задача. Пример книги — оконная система: есть иерархия окон (Window, IconWindow,
TransientWindow) и иерархия платформ (X11, PM). Наследование даёт декартово произведение:
XWindow, PMWindow, XIconWindow, PMIconWindow… Классов становится вдвое больше
с каждой новой платформой.
Решение — две иерархии вместо одной, связанные композицией.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Window {
#IWindowImpl impl
+DrawText(string s) void
+DrawRect() void
}
class IconWindow {
+DrawBorder() void
}
class TransientWindow
class IWindowImpl {
<<interface>>
+DevDrawText(string s) void
+DevDrawLine() void
}
class XWindowImpl
class PmWindowImpl
Window <|-- IconWindow
Window <|-- TransientWindow
Window o-- IWindowImpl : мост
IWindowImpl <|.. XWindowImpl
IWindowImpl <|.. PmWindowImpl
Исходник диаграммы
classDiagram
direction LR
class Window {
#IWindowImpl impl
+DrawText(string s) void
+DrawRect() void
}
class IconWindow {
+DrawBorder() void
}
class TransientWindow
class IWindowImpl {
<<interface>>
+DevDrawText(string s) void
+DevDrawLine() void
}
class XWindowImpl
class PmWindowImpl
Window <|-- IconWindow
Window <|-- TransientWindow
Window o-- IWindowImpl : мост
IWindowImpl <|.. XWindowImpl
IWindowImpl <|.. PmWindowImpl
public abstract class Window // абстракция
{
private readonly IWindowImpl _impl; // ссылка на реализацию
protected Window(IWindowImpl impl) => _impl = impl;
public void DrawRect(Point a, Point b) // операции высокого уровня
{
_impl.DevDrawLine(a, new Point(b.X, a.Y));
_impl.DevDrawLine(new Point(b.X, a.Y), b);
}
}
public sealed class IconWindow : Window // уточнённая абстракция
{
public IconWindow(IWindowImpl impl) : base(impl) { }
public void DrawBorder() => DrawRect(Point.Zero, new Point(64, 64));
}
public sealed class XWindowImpl : IWindowImpl // конкретная реализация
{
public void DevDrawLine(Point a, Point b) { }
}
Применимость. Нужно избежать постоянной привязки абстракции к реализации (например, реализация выбирается во время выполнения); и абстракция, и реализация должны расширяться подклассами независимо; изменения в реализации не должны влиять на клиентов.
Результаты. Отделяет интерфейс от реализации, устраняет комбинаторный взрыв классов, позволяет переключать реализацию в рантайме, улучшает расширяемость двух иерархий по отдельности.
Мост против адаптера. Один и тот же рисунок, разное намерение и разное время появления: адаптер применяют постфактум, чтобы подружить несовместимое; мост закладывают заранее, чтобы две оси менялись независимо.
В .NET. Классический пример —
ILoggerи провайдеры логирования: категории и уровни живут в абстракции, а вывод в консоль, файл или Seq — в реализациях. Драйверы БД черезDbConnection— тоже мост.
Блок 4. Composite — компоновщик¶
Назначение. Группирует объекты в древовидные структуры для представления иерархий «часть — целое». Позволяет клиентам работать с единичными объектами так же, как с группами.
Задача. Графический редактор: линия, текст и группа фигур должны рисоваться одинаково; группа может содержать другие группы. Клиент не должен различать лист и узел.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction TB
class IGraphic {
<<interface>>
+Draw() void
+Add(IGraphic g) void
+Remove(IGraphic g) void
}
class Line {
+Draw() void
}
class Text {
+Draw() void
}
class Picture {
-List~IGraphic~ children
+Draw() void
+Add(IGraphic g) void
}
IGraphic <|.. Line
IGraphic <|.. Text
IGraphic <|.. Picture
Picture o-- IGraphic : дети
Исходник диаграммы
classDiagram
direction TB
class IGraphic {
<<interface>>
+Draw() void
+Add(IGraphic g) void
+Remove(IGraphic g) void
}
class Line {
+Draw() void
}
class Text {
+Draw() void
}
class Picture {
-List~IGraphic~ children
+Draw() void
+Add(IGraphic g) void
}
IGraphic <|.. Line
IGraphic <|.. Text
IGraphic <|.. Picture
Picture o-- IGraphic : дети
public interface IGraphic { void Draw(); }
public sealed class Line : IGraphic // лист
{
public void Draw() { }
}
public sealed class Picture : IGraphic // составной объект
{
private readonly List<IGraphic> _children = new();
public void Add(IGraphic child) => _children.Add(child);
public void Remove(IGraphic child) => _children.Remove(child);
public void Draw()
{
foreach (var child in _children) child.Draw(); // делегирование детям
}
}
Применимость. Нужно представить иерархию «часть — целое»; клиенты должны единообразно трактовать составные и индивидуальные объекты.
Главный компромисс паттерна, о котором прямо пишет книга. Где объявлять Add/Remove?
- В общем интерфейсе — клиент действительно единообразен, но у листа появляются операции, которые он не может выполнить (нарушение LSP и ISP из лекции 2). Книга предлагает делать их безопасным «ничего не делаю» или бросать исключение.
- Только в составном классе — типобезопасно, но клиенту приходится различать лист и узел, ради чего паттерн и затевался.
Выбор — компромисс между прозрачностью и безопасностью. Это отличный пример того, что паттерн не отменяет принципы, а заставляет осознанно выбирать сторону.
Результаты. Иерархия из простых и составных объектов; упрощение клиента; лёгкое добавление новых видов компонентов. ⚠️ Проектирование становится слишком общим: трудно ограничить, какие компоненты можно вкладывать друг в друга.
В .NET. Дерево визуальных элементов WPF/MAUI,
Expression-деревья, узлы Roslyn, файловая система (DirectoryInfo/FileInfo) — везде компоновщик.
Блок 5. Decorator — декоратор¶
Назначение. Динамически наделяет объект новыми обязанностями. Является гибкой альтернативой порождению подклассов.
Задача. Пример книги — рамка и полоса прокрутки для текстового поля. Через наследование
получаем ScrollableTextView, BorderedTextView, BorderedScrollableTextView — снова
комбинаторный взрыв. Декоратор оборачивает объект и добавляет поведение до или после
делегирования.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction TB
class IVisualComponent {
<<interface>>
+Draw() void
}
class TextView {
+Draw() void
}
class Decorator {
<<abstract>>
#IVisualComponent component
+Draw() void
}
class BorderDecorator {
-int width
+Draw() void
-DrawBorder() void
}
class ScrollDecorator {
+Draw() void
}
IVisualComponent <|.. TextView
IVisualComponent <|.. Decorator
Decorator <|-- BorderDecorator
Decorator <|-- ScrollDecorator
Decorator o-- IVisualComponent : обёрнутый компонент
Исходник диаграммы
classDiagram
direction TB
class IVisualComponent {
<<interface>>
+Draw() void
}
class TextView {
+Draw() void
}
class Decorator {
<<abstract>>
#IVisualComponent component
+Draw() void
}
class BorderDecorator {
-int width
+Draw() void
-DrawBorder() void
}
class ScrollDecorator {
+Draw() void
}
IVisualComponent <|.. TextView
IVisualComponent <|.. Decorator
Decorator <|-- BorderDecorator
Decorator <|-- ScrollDecorator
Decorator o-- IVisualComponent : обёрнутый компонент
public abstract class VisualDecorator : IVisualComponent
{
private readonly IVisualComponent _inner;
protected VisualDecorator(IVisualComponent inner) => _inner = inner;
public virtual void Draw() => _inner.Draw(); // делегирование
}
public sealed class BorderDecorator : VisualDecorator
{
private readonly int _width;
public BorderDecorator(IVisualComponent inner, int width) : base(inner) => _width = width;
public override void Draw()
{
base.Draw(); // сначала сам компонент
DrawBorder(_width); // потом добавленная обязанность
}
private void DrawBorder(int width) { }
}
// Сборка обязанностей во время выполнения
IVisualComponent view = new BorderDecorator(new ScrollDecorator(new TextView()), width: 1);
# Тот же паттерн «в лоб»: обёртка вокруг объекта с тем же интерфейсом
class VisualDecorator:
def __init__(self, inner: VisualComponent) -> None:
self._inner = inner
def draw(self) -> None:
self._inner.draw()
class BorderDecorator(VisualDecorator):
def __init__(self, inner: VisualComponent, width: int = 1) -> None:
super().__init__(inner)
self._width = width
def draw(self) -> None:
super().draw()
self._draw_border(self._width)
view = BorderDecorator(ScrollDecorator(TextView()), width=1)
Декоратор в Python: тот же смысл, другая форма¶
Это место надо разобрать отдельно, потому что в Python слово «декоратор» означает
синтаксическую конструкцию языка — @-аннотацию над функцией или классом. Идея
та же самая (обёртка, добавляющая обязанности), но объект обёртывания другой:
в GoF оборачивают объект, в Python чаще всего — вызываемое.
import functools, time
def timed(func): # декоратор — функция над функцией
@functools.wraps(func) # сохраняет имя и docstring обёрнутого
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.3f}s")
return wrapper
def retry(times: int): # декоратор с параметром — три уровня
def decorate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except TimeoutError:
if attempt == times - 1: raise
return wrapper
return decorate
@retry(times=3) # применяются снизу вверх:
@timed # retry(timed(fetch_report))
def fetch_report(report_id: int) -> Report:
...
Что здесь важно проговорить студентам:
- Порядок применения — снизу вверх: ближайший к функции декоратор оборачивает первым.
Ровно как порядок вложенных обёрток в C#:
new Retry(new Timed(component)). @-декоратор — это простоfetch_report = retry(3)(timed(fetch_report)). Синтаксис, а не магия.functools.wrapsнужен, чтобы обёртка не подменяла имя, документацию и сигнатуру оригинала — иначе отладка и рефлексия ломаются.- Декорировать можно и классы, и методы;
functools.lru_cache— декоратор из стандартной библиотеки, добавляющий кеширование, то есть обязанность.
Чем это отличается от GoF-декоратора. GoF требует, чтобы обёртка имела тот же
интерфейс, что и компонент, и чтобы её можно было подставить везде вместо него.
Питоновский декоратор функции этого требования не нарушает (обёртка вызывается так же),
но применяется он к вызываемому объекту, а не к экземпляру класса. Для объектов
в Python обычно берут либо явную обёртку, как в примере выше, либо __getattr__:
class LoggingProxy: # прозрачная обёртка над ЛЮБЫМ объектом
def __init__(self, inner): self._inner = inner
def __getattr__(self, name): # вызывается только для отсутствующих атрибутов
attr = getattr(self._inner, name)
if not callable(attr): return attr
def wrapped(*args, **kwargs):
print(f"-> {name}")
return attr(*args, **kwargs)
return wrapped
В C# так не сделать без генерации кода: обёртку приходится писать руками по интерфейсу
(или брать DispatchProxy, source-генератор, перехватчики DI). Зато компилятор гарантирует,
что вы реализовали весь контракт, — в Python эта гарантия отсутствует.
Применимость. Нужно динамически добавлять обязанности отдельным объектам, не затрагивая другие; обязанности можно снимать; расширение наследованием непрактично из-за числа комбинаций.
Результаты. Больше гибкости, чем у статического наследования; позволяет не «раздувать» базовый класс редко нужными возможностями. ⚠️ Декоратор и компонент не тождественны: проверка типа обёрнутого объекта даст декоратор, а не исходный класс. И появляется множество мелких похожих объектов — систему труднее изучать и отлаживать.
Декоратор против стратегии. Декоратор меняет оболочку объекта снаружи, стратегия — его внутренности (об этом в лекции 7).
В .NET. Потоки:
new GZipStream(new BufferedStream(new FileStream(...)))— три обёртки, каждая добавляет одну обязанность.DelegatingHandlerвHttpClient— конвейер декораторов (повторы, логирование, авторизация). В Python декораторы функций через@— синтаксическая поддержка той же идеи на уровне языка: обёртка вокруг вызываемого объекта. В Kotlin делегированиеbyпозволяет написать декоратор в одну строку, не переписывая все методы интерфейса вручную.
Блок 6. Facade — фасад¶
Назначение. Предоставляет унифицированный интерфейс к набору интерфейсов подсистемы. Определяет интерфейс более высокого уровня, облегчающий работу с подсистемой.
Задача. Пример книги — компилятор: сканер, парсер, генератор кода, оптимизатор. Большинству клиентов нужен один вызов «скомпилируй файл», а не знание о всех классах.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class Compiler {
+Compile(string source) byte[]
}
class Scanner
class Parser
class ProgramNodeBuilder
class CodeGenerator
Client --> Compiler : знает только фасад
Compiler ..> Scanner
Compiler ..> Parser
Compiler ..> ProgramNodeBuilder
Compiler ..> CodeGenerator
Исходник диаграммы
public sealed class Compiler // фасад
{
public byte[] Compile(string source)
{
var tokens = new Scanner(source).Scan();
var tree = new Parser().Parse(tokens);
return new CodeGenerator().Generate(tree);
}
}
Применимость. Нужно предоставить простой интерфейс к сложной подсистеме; есть много зависимостей между клиентами и классами реализации; требуется разложить подсистему на слои, общающиеся через фасады.
Результаты. Изолирует клиентов от компонентов подсистемы, ослабляет связанность, но не запрещает доступ к внутренним классам напрямую — те, кому нужна тонкая настройка, по-прежнему могут работать с подсистемой. Это отличает фасад от адаптера: адаптер меняет интерфейс, фасад упрощает его.
⚠️ Антипаттерн рядом: фасад, который постепенно оброс логикой и превратился в God Object. Фасад делегирует, а не решает.
Блок 7. Proxy — заместитель¶
Назначение. Подменяет другой объект для контроля доступа к нему.
Задача. Пример книги — документ с изображениями: открывать документ быстро, а картинки загружать только когда их действительно надо нарисовать. Клиент об этом знать не должен.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class IGraphic {
<<interface>>
+Draw(Point at) void
+GetExtent() Size
}
class Image {
-string fileName
+Draw(Point at) void
}
class ImageProxy {
-string fileName
-Image image
-Size extent
+Draw(Point at) void
}
IGraphic <|.. Image
IGraphic <|.. ImageProxy
ImageProxy o-- Image : создаёт при первом обращении
Исходник диаграммы
classDiagram
direction LR
class IGraphic {
<<interface>>
+Draw(Point at) void
+GetExtent() Size
}
class Image {
-string fileName
+Draw(Point at) void
}
class ImageProxy {
-string fileName
-Image image
-Size extent
+Draw(Point at) void
}
IGraphic <|.. Image
IGraphic <|.. ImageProxy
ImageProxy o-- Image : создаёт при первом обращении
public sealed class ImageProxy : IGraphic
{
private readonly string _fileName;
private Image? _image; // ещё не загружен
private Size _extent; // дешёвая часть данных
public ImageProxy(string fileName, Size extent) => (_fileName, _extent) = (fileName, extent);
public Size GetExtent() => _image?.GetExtent() ?? _extent; // без загрузки файла
public void Draw(Point at)
{
_image ??= new Image(_fileName); // загрузка по требованию
_image.Draw(at);
}
}
class ImageProxy:
def __init__(self, file_name: str, extent: Size) -> None:
self._file_name, self._extent, self._image = file_name, extent, None
def get_extent(self) -> Size:
return self._image.get_extent() if self._image else self._extent
def draw(self, at: Point) -> None:
if self._image is None:
self._image = Image(self._file_name) # загрузка при первом рисовании
self._image.draw(at)
# для «умных ссылок» в Python часто хватает __getattr__ — заместитель получается
# прозрачным для любого интерфейса, без ручного перечисления методов
Виды заместителей (по книге).
| Вид | Что делает |
|---|---|
| Удалённый | представляет объект в другом адресном пространстве |
| Виртуальный | создаёт «тяжёлый» объект по требованию |
| Защищающий | контролирует права доступа |
| «Умная» ссылка | подсчёт ссылок, загрузка в память, блокировки |
Результаты. Вводит уровень косвенности: можно скрыть, что объект удалённый, отложить его создание или добавить проверки. ⚠️ Косвенность имеет цену — лишний вызов и усложнение отладки.
В .NET.
RealProxy/DispatchProxy, прокси EF Core для ленивой загрузки навигационных свойств, gRPC- и HTTP-клиенты, сгенерированные по контракту, — удалённые заместители. Кеширующие и повторяющие обёртки поверх репозиториев — «умные ссылки».
Блок 8. Flyweight — приспособленец¶
Назначение. Применяет разделение (совместное использование) для эффективной поддержки множества мелких объектов.
Задача. Пример книги — текстовый редактор, где каждый символ является объектом. Документ на 100 000 символов не может держать 100 000 объектов с полным состоянием.
Ключевая идея — разделить состояние на два вида:
- внутреннее (intrinsic) — не зависит от контекста, хранится в приспособленце и разделяется всеми: код символа, глиф, шрифт;
- внешнее (extrinsic) — зависит от контекста, хранится у клиента и передаётся в операции: позиция на странице, стиль абзаца.
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#c9a227'}}}%%
classDiagram
direction LR
class GlyphFactory {
-Dictionary~char,Glyph~ pool
+GetGlyph(char c) Glyph
}
class Glyph {
<<interface>>
+Draw(GlyphContext ctx) void
}
class CharacterGlyph {
-char symbol
+Draw(GlyphContext ctx) void
}
GlyphFactory o-- Glyph : пул разделяемых объектов
Glyph <|.. CharacterGlyph
Client ..> GlyphFactory
Client ..> Glyph : передаёт внешнее состояние
Исходник диаграммы
classDiagram
direction LR
class GlyphFactory {
-Dictionary~char,Glyph~ pool
+GetGlyph(char c) Glyph
}
class Glyph {
<<interface>>
+Draw(GlyphContext ctx) void
}
class CharacterGlyph {
-char symbol
+Draw(GlyphContext ctx) void
}
GlyphFactory o-- Glyph : пул разделяемых объектов
Glyph <|.. CharacterGlyph
Client ..> GlyphFactory
Client ..> Glyph : передаёт внешнее состояние
public sealed class GlyphFactory
{
private readonly Dictionary<char, CharacterGlyph> _pool = new();
public CharacterGlyph GetGlyph(char symbol) // объект создаётся один раз на символ
{
if (!_pool.TryGetValue(symbol, out var glyph))
_pool[symbol] = glyph = new CharacterGlyph(symbol);
return glyph;
}
}
public sealed class CharacterGlyph
{
private readonly char _symbol; // внутреннее состояние
public CharacterGlyph(char symbol) => _symbol = symbol;
public void Draw(Point position, Font font) { } // внешнее — приходит параметрами
}
from functools import lru_cache
@lru_cache(maxsize=None) # пул разделяемых объектов — одной строкой
def glyph_for(symbol: str) -> "CharacterGlyph":
return CharacterGlyph(symbol)
class CharacterGlyph:
__slots__ = ("symbol",) # запрещаем словарь атрибутов: объект компактнее
def __init__(self, symbol: str) -> None: self.symbol = symbol
def draw(self, position: Point, font: Font) -> None: ...
Применимость. Объектов очень много; затраты на хранение высоки; большую часть состояния можно сделать внешним; после вынесения внешнего состояния многие группы объектов заменяются небольшим числом разделяемых; приложение не зависит от идентичности объектов.
Результаты. Экономия памяти тем больше, чем больше объектов и чем больше состояния удалось вынести наружу. ⚠️ Плата — время на вычисление или передачу внешнего состояния и потеря идентичности объектов (сравнивать по ссылке больше нельзя).
В .NET. Интернирование строк (
string.Intern),ArrayPool<T>, кеш метаданных Roslyn. В Python малые целые и короткие строки закешированы интерпретатором — приспособленец встроен в рантайм.
Подробнее — в материале для самостоятельной работы: там приспособленец разбирается вместе с посетителем и интерпретатором.
Блок 9. Как их различать и выбирать¶
Три близнеца: компоновщик, декоратор, заместитель¶
Диаграммы у них почти одинаковые — объект, реализующий интерфейс и хранящий ссылку на объект того же интерфейса. Книга посвящает этому сравнению отдельный раздел, и вот суть:
| Что делает с обёрнутым объектом | Сколько объектов внутри | |
|---|---|---|
| Компоновщик | группирует, чтобы клиент не различал часть и целое | много |
| Декоратор | добавляет обязанности, сохраняя интерфейс | один |
| Заместитель | контролирует доступ, ничего не добавляя к обязанностям | один |
Проверочный вопрос: зачем здесь косвенность? Ради единообразия дерева — компоновщик. Ради нового поведения — декоратор. Ради контроля доступа, ленивости или удалённости — заместитель.
Ещё три пары, которые путают¶
- Адаптер против моста. Адаптер применяют после того, как классы спроектированы, чтобы совместить несовместимое. Мост проектируют до, чтобы две оси развивались независимо.
- Фасад против адаптера. Адаптер даёт другой интерфейс к тому же объекту; фасад даёт более простой интерфейс к целой подсистеме.
- Декоратор против стратегии. Декоратор меняет объект снаружи (оболочка), стратегия меняет его изнутри (подставленный алгоритм). Декоратор годится, когда компонент тяжело изменить; стратегия — когда компонент изначально предусматривает подмену части поведения.
Алгоритм выбора¶
- Назовите, что должно меняться: интерфейс, реализация, состав, обязанности, доступ, память.
- Найдите строку в таблице переменных аспектов (блок 1).
- Проверьте намерение по таблице близнецов выше.
- Оцените плату: число классов, косвенность, потеря идентичности, сложность отладки.
Ошибки¶
- Декоратор с расширенным интерфейсом. Как только обёртка добавляет новые публичные методы, клиенту приходится знать её тип — паттерн сломан.
- Фасад, ставший God Object. Фасад делегирует, а не реализует.
- Заместитель, меняющий поведение. Если обёртка меняет результат, это декоратор, и называть его надо честно.
- Приспособленец без замера. Экономия памяти — гипотеза, которую нужно проверять профилировщиком, а не предполагать.
- Мост при одной реализации. Классическая теоретическая общность.
Домашнее задание¶
- В своём проекте найти место, где интерфейс чужого класса не подходит, и написать объектный адаптер. Отдельно объяснить, почему классовый вариант здесь не подошёл бы.
- Реализовать конвейер из трёх декораторов над одним интерфейсом (например, репозиторий: кеширование, логирование, повторы) и показать в тесте, что порядок обёрток меняет поведение.
- Построить компоновщик для древовидной структуры своей предметной области; в отчёте явно выбрать сторону в компромиссе «прозрачность против безопасности» и обосновать.
- Для одного тяжёлого ресурса написать виртуальный заместитель с ленивой загрузкой, измерить время старта до и после.
- Все структуры — диаграммами классов в Mermaid с именами участников по книге (Target, Adaptee, Component, Decorator, Subject, RealSubject, Flyweight).