Pattern Fusion — Designing a Document Editor
A single pattern almost never ships alone. Real systems fuse them — one design
problem after another, each answered by a pattern, until a whole architecture emerges. In this chapter
we design a word-processor (call it Lexi, after the classic Gang-of-Four case study) from a blank
page. Every requirement raises a design question; we look at the naive answer, watch it break under one
concrete change request, and land on the pattern that fixes it — in idiomatic C#. By the end you'll see
the nine patterns cooperating around a single Document core.
Chapters 3–5 taught the patterns one at a time, in isolation. That is not how you meet them in the wild. The skill this chapter builds is the one interviews and real projects actually test: given a messy requirement, recognise which pattern the situation is asking for — and how it plugs into the ones already there.
Patterns are a vocabulary, not a checklist
A beginner reads the 23 patterns as 23 gadgets and asks "which one do I use?" That is the wrong frame. Patterns are a shared vocabulary for design conversations. When a teammate says "make the border a Decorator so scrollbars stack the same way," a whole paragraph of intent travels in one word. The value is compression: named solutions let a team reason about structure out loud.
And because each pattern solves one kind of problem, a realistic feature touches several at once. Saving a document that you can also spell-check, undo, and render on two operating systems is not one problem — it is five, braided together. So the patterns braid too. Watch for three kinds of relationship as we go:
Think of patterns like chess openings. A club player memorises individual moves; a strong player thinks in named sequences ("Sicilian Defence") and in how one opening transposes into another. The names don't win the game — but they let you plan several moves ahead and talk about the plan.
The editor brief
Here is the product brief, distilled from the lecture slides and rounded out to the full classic document-editor exercise. Read each line as a design pressure, not just a feature — the phrasing hints at what will change later, and change is what patterns defend against.
| # | Requirement | The pressure it creates | Pattern we'll reach for |
|---|---|---|---|
| 1 | A document is a tree of characters, images, rows, columns, tables and pages, edited uniformly. | Treat a single glyph and a whole table the same way. | Composite |
| 2 | A page holds hundreds of thousands of characters. | One object per character would blow up memory. | Flyweight |
| 3 | Build documents from headers, paragraphs and sections fluently. | Assemble a complex tree without a giant constructor. | Builder |
| 4 | Text must flow into lines — and different quality/speed trade-offs are wanted. | Swap the line-breaking algorithm without touching the document. | Strategy |
| 5 | Optional borders and scrollbars around any element. | Add responsibilities to individual objects, stackably, at run time. | Decorator |
| 6 | Match multiple look-and-feel standards (Windows, macOS…). | Create families of matching widgets without hard-coding one. | Abstract Factory |
| 7 | Run on multiple window systems. | Vary the widget abstraction and the platform independently. | Bridge |
| 8 | Every edit is undoable and redoable. | Encapsulate actions so they can be logged, queued and reversed. | Command + Memento |
| 9 | Spell-check and hyphenation sweep the whole document. | Walk the tree, and add new analyses without editing element classes. | Iterator + Visitor |
We'll take them in an order that lets each pattern build on the last. Skip ahead with the sidebar, or play with the design explorer and collaboration map once you've read a few.
1 · Document structure → Composite
Composite · structural
The requirement. A document isn't flat text. A character sits in a line, lines stack into a column, columns arrange on a page, and an image or a table can appear anywhere a character can. The editor must let the user select, move, draw and measure any of these — a single letter or an entire nested table — with the same operations.
The design question. How do we treat a leaf (one character) and a container (a table full of characters) uniformly, so that code which lays out or draws the document never has to ask "is this one thing or a group of things?"
void Draw(object node) {
if (node is Character c) DrawChar(c);
else if (node is Image img) DrawImage(img);
else if (node is Row row) foreach (var k in row.Kids) Draw(k);
else if (node is Column col) foreach (var k in col.Kids) Draw(k);
// ...a new element type = edit this method, and Measure(), and Select()...
}
if/else ladder. Adding a Table means editing them all. This is exactly the
Open/Closed violation from Chapter 2.Draw(page); // page recurses into columns,
// columns into rows, rows into glyphs —
// the caller never branches on type.
The solution. Define one abstraction — call it Glyph, the GoF name for
"anything that appears in the document" — with the operations every element supports. A leaf implements them
directly; a composite implements them by delegating to its children. This is the
Composite pattern: compose objects into tree
structures, then treat individual objects and compositions identically.
public interface IGlyph
{
void Draw(Window w); // paint yourself
Rect Bounds(); // where you are
bool Intersects(Point p); // hit-testing for the mouse
// Child-management: present on the interface so callers stay uniform.
void Insert(IGlyph child, int at);
void Remove(IGlyph child);
IGlyph Child(int i);
IEnumerable<IGlyph> Children();
}
// A leaf has no children, so child-ops are no-ops (or throw).
public sealed class Character : IGlyph
{
private readonly char _c;
public Character(char c) => _c = c;
public void Draw(Window w) => w.DrawChar(_c, Bounds());
public Rect Bounds() => /* font metrics */ new Rect();
public bool Intersects(Point p) => Bounds().Contains(p);
public void Insert(IGlyph child, int at) =>
throw new NotSupportedException("A character has no children.");
public void Remove(IGlyph child) => throw new NotSupportedException();
public IGlyph Child(int i) => throw new NotSupportedException();
public IEnumerable<IGlyph> Children() => Array.Empty<IGlyph>();
}
// A composite forwards each operation to its children.
public class Row : IGlyph // Column, Page, Table all look like this
{
private readonly List<IGlyph> _kids = new();
public void Draw(Window w) { foreach (var k in _kids) k.Draw(w); }
public Rect Bounds() => Rect.Union(_kids.Select(k => k.Bounds()));
public bool Intersects(Point p) => _kids.Any(k => k.Intersects(p));
public void Insert(IGlyph child, int at) => _kids.Insert(at, child);
public void Remove(IGlyph child) => _kids.Remove(child);
public IGlyph Child(int i) => _kids[i];
public IEnumerable<IGlyph> Children() => _kids;
}
// The client builds a tree and treats every node the same way.
IGlyph page = new Page();
IGlyph col = new Column();
IGlyph row = new Row();
row.Insert(new Character('H'), 0);
row.Insert(new Character('i'), 1);
row.Insert(new Image("logo.png"), 2); // an image sits where a char can
col.Insert(row, 0);
page.Insert(col, 0);
page.Draw(window); // recurses to every leaf — no type checks
Console.WriteLine(page.Bounds()); // whole-page extent, computed by delegation
IGlyph); leaves and containers implement it. The
child link is a composition (the composite owns its children), and children are themselves
IGlyph — so the tree can nest to any depth.Putting Insert/Remove on the shared interface makes every node look the same
(transparency) — but lets someone call character.Insert(...), which is meaningless.
The alternative — child-ops only on composites — is safe but forces callers to type-check again,
losing the whole point. GoF choose transparency, and so do we: leaves throw
NotSupportedException. Choose your poison consciously.
2 · A million characters → Flyweight
Flyweight · structural
The requirement. Composite told us every character is an object. Lovely — until
you open a 300-page report. That's easily a million Character objects. If each carries its own
font, size, colour and position, the document won't fit in memory, and just allocating them stutters the UI.
The design question. The letter 'e' appears tens of thousands of times, always
drawn the same way. Why store its font and shape ten thousand times? Can many 'e' positions in
the document share one 'e' object?
The naive option and why it fails. Give each Character fields for
char, Font, Colour, Point position. Simple — and it
multiplies the heaviest data (the font/shape) by the number of occurrences. The change request that kills it:
"support long documents on a laptop."
The solution. Split each character's data into two kinds:
- Intrinsic — shared, context-free: which glyph it is and its shape/metrics for a font. One shared object per (char, font) pair.
- Extrinsic — context-dependent: where this occurrence sits, computed on the fly and passed in when drawing.
That is Flyweight: share fine-grained objects by moving their varying state outside them. A factory hands out shared instances so identical glyphs are created once.
// The flyweight: immutable, context-free, safe to share.
public sealed class CharGlyph
{
private readonly char _symbol;
public CharGlyph(char symbol) => _symbol = symbol;
// Extrinsic state (position, font) is a PARAMETER, not a field.
public void Draw(Window w, Point where, Font font) =>
w.DrawChar(_symbol, where, font);
}
// The factory guarantees one shared object per character.
public sealed class GlyphFactory
{
private readonly Dictionary<char, CharGlyph> _pool = new();
public CharGlyph Get(char c)
{
if (!_pool.TryGetValue(c, out var g))
_pool[c] = g = new CharGlyph(c); // created once, reused forever
return g;
}
public int Distinct => _pool.Count;
}
var factory = new GlyphFactory();
foreach (var (c, pos, font) in typedText) // "eee..." reuses ONE 'e'
factory.Get(c).Draw(window, pos, font);
Console.WriteLine($"Rendered thousands of chars from {factory.Distinct} glyph objects");
// e.g. "Rendered thousands of chars from 62 glyph objects"
They seem to fight — Composite says "each glyph is a node in the tree," Flyweight says "share glyph objects." The reconciliation: the tree holds lightweight position/index nodes, and each node refers to a shared flyweight for its shape. The shared object can't know its position (many parents share it), so position is passed as extrinsic state at draw time. Chapter 12 (Confusable Patterns) has the full side-by-side.
String interning (string.Intern) is Flyweight: identical string literals
share one instance. Brushes.Red and Pens.Black in GDI+/WPF are cached shared
flyweights too — you never new a red brush per shape.
3 · Assembling documents → Builder
Builder · creational
The requirement. Programs and templates need to construct documents: a report template adds a title header, an intro paragraph, then sections with headings and body text. The slides call this out explicitly, and suggest a fluent interface for ease of use.
The design question. How do we assemble a deeply nested Composite tree without either a
monstrous constructor (new Page(new Column(new Row(new Character(... — unreadable) or scattering
the tree-wiring logic across the whole app?
The solution. Builder: separate
the construction of a complex object from its representation. A DocumentBuilder exposes
intent-revealing steps (AddHeading, AddParagraph, BeginSection) and
hides the Composite plumbing. Return this from each step and you get the fluent chain the slides
ask for.
public sealed class DocumentBuilder
{
private readonly Page _page = new();
private Column _current;
public DocumentBuilder() => _page.Insert(_current = new Column(), 0);
public DocumentBuilder AddHeading(string text, int level = 1)
{
var row = new Row();
foreach (var c in text) row.Insert(new Character(c), row.Count);
row.Style = $"h{level}";
_current.Insert(row, _current.Count);
return this; // ← fluent: enables chaining
}
public DocumentBuilder AddParagraph(string text)
{
var row = new Row();
foreach (var c in text) row.Insert(new Character(c), row.Count);
_current.Insert(row, _current.Count);
return this;
}
public DocumentBuilder BeginSection()
{
_page.Insert(_current = new Column(), _page.Count);
return this;
}
public IGlyph Build() => _page; // hand back the finished tree
}
IGlyph doc = new DocumentBuilder()
.AddHeading("Quarterly Report", level: 1)
.AddParagraph("Revenue grew 12% this quarter.")
.BeginSection()
.AddHeading("Regional Breakdown", level: 2)
.AddParagraph("EMEA led with 40% of new bookings.")
.Build();
Both are creational and both appear in this design, but they answer different questions. Builder assembles one complex product step by step (the document tree). Abstract Factory creates families of related products in one shot (a whole matching widget set). You'll use Builder to make documents and Abstract Factory to make the UI around them.
4 · Line-breaking → Strategy
Strategy · behavioral
The requirement. Text has to flow: given a column width, decide where each line breaks. And there is no single right algorithm — a fast greedy break is fine while typing, but for print you want a slower, higher-quality break (like TeX's whole-paragraph optimiser) that minimises ragged edges.
The design question. The document structure (Composite) shouldn't change when we swap line-breaking algorithms. How do we make the formatting algorithm a pluggable, replaceable thing?
class Column {
public void Reflow(int width) {
// 40 lines of greedy line-breaking, hard-wired.
// Want TeX-quality? Edit this class.
// Want to A/B two algorithms? You can't.
}
}
Column now has two reasons to change (structure and formatting) — an SRP smell.column.Compositor = new SimpleCompositor(); // fast
// later, for print:
column.Compositor = new TeXCompositor(); // pretty
column.Reflow(width); // same call, new behaviour
The solution. Pull line-breaking into its own interface — GoF names it the
Compositor — and let a Column hold a reference to one.
Strategy: define a family of interchangeable
algorithms and make them swappable at run time.
public interface ICompositor // the Strategy
{
IReadOnlyList<Line> Compose(IReadOnlyList<IGlyph> glyphs, int width);
}
public sealed class SimpleCompositor : ICompositor // greedy: fast
{
public IReadOnlyList<Line> Compose(IReadOnlyList<IGlyph> g, int width)
=> GreedyBreak(g, width);
}
public sealed class TeXCompositor : ICompositor // optimal: pretty
{
public IReadOnlyList<Line> Compose(IReadOnlyList<IGlyph> g, int width)
=> MinimiseRaggedness(g, width);
}
public class Column : IGlyph
{
public ICompositor Compositor { get; set; } = new SimpleCompositor();
public void Reflow(int width) => Layout(Compositor.Compose(Children().ToList(), width));
// ...IGlyph members omitted...
}
List<T>.Sort(IComparer<T>) takes a Strategy — the comparer is the swappable
algorithm. System.Text.Json converters and ASP.NET's authorization handlers are Strategy
objects too.
5 · Borders & scrollbars → Decorator
Decorator · structural
The requirement. The user should be able to put a border around any element, and a scrollbar on any view — and combine them (a bordered, scrollable text area). These embellishments are optional and chosen at run time.
The design question. How do we add a border to something without making "bordered" a new subclass of every element, and without the element even knowing it's been bordered?
The naive option and why it fails. Subclass: BorderedColumn,
ScrollableColumn, BorderedScrollableColumn… Now add "shadow" and the subclass count
explodes combinatorially. The change request that kills it: "borders should also work on images and
tables." You'd re-derive the whole hierarchy.
The solution. A border is a glyph that wraps another glyph: it draws the
child, then draws a frame around it. Because it implements IGlyph, anything that accepts a glyph
accepts a bordered glyph — including another decorator. That is
Decorator: attach responsibilities to an object
dynamically by wrapping it in an object of the same interface. Decorator deliberately shares
IGlyph with Composite, which is why decorators drop straight into the tree.
// Base decorator: forwards everything to the wrapped glyph.
public abstract class GlyphDecorator : IGlyph
{
protected readonly IGlyph Inner;
protected GlyphDecorator(IGlyph inner) => Inner = inner;
public virtual void Draw(Window w) => Inner.Draw(w);
public virtual Rect Bounds() => Inner.Bounds();
public virtual bool Intersects(Point p) => Inner.Intersects(p);
// child-ops delegate to Inner as well...
}
public sealed class Border : GlyphDecorator
{
private readonly int _width;
public Border(IGlyph inner, int width = 1) : base(inner) => _width = width;
public override void Draw(Window w) // add behaviour, then call inner
{
Inner.Draw(w);
w.DrawRect(Bounds().Inflate(_width));
}
public override Rect Bounds() => base.Bounds().Inflate(_width);
}
public sealed class Scroller : GlyphDecorator
{
public Scroller(IGlyph inner) : base(inner) { }
public override void Draw(Window w) { Inner.Draw(w); w.DrawScrollBars(Bounds()); }
}
IGlyph view = new Column();
view = new Border(view, 2); // wrap once
view = new Scroller(view); // wrap again — order = visual layering
view.Draw(window); // scroller draws border draws column
IGlyph and is an
IGlyph. Wrapping order is layering order.Streams are the textbook case: new GZipStream(new BufferedStream(new FileStream(...))) —
each wraps a Stream and adds one responsibility. ASP.NET Core middleware is a
Decorator-style pipeline around the request.
6 · Look-and-feel → Abstract Factory
Abstract Factory · creational
The requirement. The editor's chrome (buttons, scrollbars, menus) must match the host platform's look-and-feel — Windows widgets on Windows, macOS widgets on macOS — and switching the whole set should be one decision, not a thousand.
The design question. How do we build a UI that is consistent (never a macOS
button next to a Windows scrollbar) without new WindowsButton() hard-coded all over the code?
The naive option and why it fails. Scatter if (os == Windows) new WindowsScrollBar()
else new MacScrollBar() at every creation site. Add a Linux theme and you edit every site; miss one and
you get a mismatched widget. The change request that kills it: "add a third look-and-feel."
The solution. One factory interface with a create-method per widget kind, and one concrete factory per look-and-feel. Client code depends only on the interface, so a whole family swaps by swapping one object. Abstract Factory: provide an interface for creating families of related objects without naming their concrete classes.
public interface IGuiFactory // the Abstract Factory
{
IButton CreateButton();
IScrollBar CreateScrollBar();
IMenu CreateMenu();
}
public sealed class WindowsFactory : IGuiFactory
{
public IButton CreateButton() => new WindowsButton();
public IScrollBar CreateScrollBar() => new WindowsScrollBar();
public IMenu CreateMenu() => new WindowsMenu();
}
public sealed class MacFactory : IGuiFactory
{
public IButton CreateButton() => new MacButton();
public IScrollBar CreateScrollBar() => new MacScrollBar();
public IMenu CreateMenu() => new MacMenu();
}
public sealed class Editor
{
private readonly IGuiFactory _ui; // doesn't know which family
public Editor(IGuiFactory ui) => _ui = ui;
public void BuildChrome()
{
var save = _ui.CreateButton(); // guaranteed to match...
var scroll = _ui.CreateScrollBar();// ...the rest of the family
}
}
// Chosen ONCE at startup:
IGuiFactory ui = OperatingSystem.IsMacOS() ? new MacFactory() : new WindowsFactory();
var editor = new Editor(ui);
You only need one factory instance for the whole app, so it is classically a Singleton (or, in modern C#, a single registration in the dependency-injection container — the DI container is itself a giant, configurable abstract factory). See the collaboration map.
7 · Multiple window systems → Bridge
Bridge · structural
The requirement. Abstract Factory handled the look. But the editor's
Window abstraction (with kinds like ApplicationWindow, IconWindow,
DialogWindow) must also run over different windowing back-ends (Win32, X11, a headless
test surface) that actually push pixels.
The design question. Two things vary independently: the kind of window (application vs dialog…) and the platform that renders it. How do we avoid a class per combination?
class Win32ApplicationWindow { }
class X11ApplicationWindow { }
class Win32DialogWindow { }
class X11DialogWindow { }
// 3 window kinds × 3 platforms = 9 classes.
// Add a platform → +3. Add a window kind → +3. Multiplicative.
var win = new DialogWindow(new X11WindowImpl());
// 3 kinds + 3 platforms = 6 classes, and they compose.
// Add a platform → +1. Add a kind → +1. Additive.
The solution. Split into two hierarchies. The abstraction
(Window and its kinds) holds a reference to an implementor
(IWindowImpl, one per platform) and delegates the low-level work to it.
Bridge: decouple an abstraction from its
implementation so the two can vary independently.
public interface IWindowImpl // Implementor — the platform
{
void DeviceRect(double x0, double y0, double x1, double y1);
void DeviceText(string s, double x, double y);
}
public sealed class X11WindowImpl : IWindowImpl { /* X11 calls */ }
public sealed class Win32WindowImpl : IWindowImpl { /* Win32 calls */ }
public abstract class Window // Abstraction — the window kind
{
protected readonly IWindowImpl Impl; // ← the bridge
protected Window(IWindowImpl impl) => Impl = impl;
public void DrawRect(Rect r) => // high-level op, platform-agnostic
Impl.DeviceRect(r.X0, r.Y0, r.X1, r.Y1);
public abstract void DrawContents();
}
public sealed class DialogWindow : Window
{
public DialogWindow(IWindowImpl impl) : base(impl) { }
public override void DrawContents() => Impl.DeviceText("[ OK ] [ Cancel ]", 10, 10);
}
They're not rivals here. Abstract Factory decides which family of widgets (the look). Bridge decides how any widget reaches the screen (the platform). A Mac look-and-feel can run over more than one rendering back-end. Look and platform are genuinely two axes, so you use two patterns.
8 · Undoable operations → Command + Memento
Command · behavioral
Memento · behavioral
The requirement. Every edit — insert text, delete, format bold, change font — must be undoable and redoable, from a menu, a toolbar or a keyboard shortcut. The slides note the command history is naturally a stack.
The design question. A menu item, a toolbar button and a hotkey all trigger "make bold". How do we invoke the action without the menu knowing how bold works — and, crucially, how do we reverse an arbitrary past action?
The naive option and why it fails. Put the logic in the menu's click handler:
OnBoldClick() { /* mutate selection */ }. Now the toolbar duplicates it, the hotkey duplicates it
again, and none of them can be undone — there is no object representing "the bold that just happened."
The change request that kills it: "add undo/redo."
The solution — Command. Turn each action into an object with
Execute() and Unexecute(). Menus, buttons and hotkeys just hold an
ICommand and call Execute. A history stack of executed commands gives you undo (pop
and Unexecute) and redo. Command:
encapsulate a request as an object.
public interface ICommand
{
void Execute();
void Unexecute(); // the inverse — this is what makes undo possible
}
public sealed class InsertTextCommand : ICommand
{
private readonly Document _doc;
private readonly string _text;
private readonly int _at;
public InsertTextCommand(Document doc, string text, int at)
=> (_doc, _text, _at) = (doc, text, at);
public void Execute() => _doc.Insert(_at, _text);
public void Unexecute() => _doc.Delete(_at, _text.Length); // exact inverse
}
public sealed class CommandHistory // the Invoker + undo stack
{
private readonly Stack<ICommand> _done = new();
private readonly Stack<ICommand> _undone = new();
public void Do(ICommand c) { c.Execute(); _done.Push(c); _undone.Clear(); }
public void Undo() { if (_done.Count > 0) { var c = _done.Pop(); c.Unexecute(); _undone.Push(c); } }
public void Redo() { if (_undone.Count > 0) { var c = _undone.Pop(); c.Execute(); _done.Push(c); } }
}
Where Memento comes in. InsertTextCommand can invert itself with a simple
delete. But "Format Text" across a selection with mixed existing styles has no tidy inverse — undoing
it means restoring the exact prior styling of every run. Rather than compute an inverse, the command
snapshots the affected state before it runs, and restores the snapshot on undo. That snapshot is a
Memento: capture an object's internal state
(without exposing its internals) so it can be restored later.
// The Memento: opaque to everyone except the Document that made it.
public sealed class DocMemento
{
internal DocMemento(IReadOnlyList<Run> snapshot) => State = snapshot;
internal IReadOnlyList<Run> State { get; } // internal = encapsulated
}
public partial class Document // the Originator
{
public DocMemento Save(Range r) => new(CopyRuns(r));
public void Restore(DocMemento m) => ReplaceRuns(m.State);
}
public sealed class FormatCommand : ICommand // the Command holds the Memento
{
private readonly Document _doc;
private readonly Range _range;
private readonly Style _style;
private DocMemento _before; // saved state for undo
public FormatCommand(Document doc, Range range, Style style)
=> (_doc, _range, _style) = (doc, range, style);
public void Execute()
{
_before = _doc.Save(_range); // snapshot BEFORE mutating
_doc.ApplyStyle(_range, _style);
}
public void Unexecute() => _doc.Restore(_before); // roll back to snapshot
}
ICommand is a first-class citizen in WPF/MAUI data binding — a button's
Command property is this pattern. Entity Framework's change tracker snapshots entity
state to support rollback — a Memento in spirit.
9 · Spell-check & hyphenation → Iterator + Visitor
Iterator · behavioral
Visitor · behavioral
The requirement. Analyses must sweep the whole document: spell-checking gathers words and flags misspellings; hyphenation finds break points; word-count tallies; find-and-replace scans. New analyses will keep being added.
The design question — two of them.
(a) How do we walk the Composite tree in reading order without every analysis re-implementing the
recursion? (b) How do we add a new analysis (say, a grammar checker) without editing every
glyph class to add a GrammarCheck() method?
Part A — Iterator. Provide one way to traverse the tree, separate from the tree's
structure. Iterator: access the elements of an
aggregate sequentially without exposing its representation. In C# this is simply
IEnumerable<IGlyph> / foreach — the language bakes Iterator in.
public static class GlyphTree
{
// yield return = a lazy Iterator, courtesy of the language.
public static IEnumerable<IGlyph> PreOrder(IGlyph root)
{
yield return root;
foreach (var child in root.Children())
foreach (var g in PreOrder(child))
yield return g;
}
}
// Any analysis just enumerates — no recursion of its own:
foreach (var g in GlyphTree.PreOrder(document)) { /* look at g */ }
Part B — Visitor. The naive fix for "add an analysis" is to add a method to
IGlyph: SpellCheck(), then Hyphenate(), then
GrammarCheck()… Every new analysis edits every glyph class — the change request
"add a readability score" touches Character, Image, Row,
Table, everything. That is the Open/Closed problem again, on the element hierarchy.
Instead, put each analysis in its own object and let glyphs accept it. Visitor: represent an operation to perform on the elements of a structure, so you can add new operations without changing the element classes.
// Glyphs gain ONE stable method: Accept. It never changes again.
public interface IGlyph // (added to the interface from §1)
{
void Accept(IGlyphVisitor v);
IEnumerable<IGlyph> Children();
// ...Draw, Bounds, etc...
}
public sealed partial class Character : IGlyph
{
public char Symbol { get; }
public void Accept(IGlyphVisitor v) => v.VisitCharacter(this);
}
public sealed partial class Image : IGlyph
{
public void Accept(IGlyphVisitor v) => v.VisitImage(this);
}
public interface IGlyphVisitor
{
void VisitCharacter(Character c);
void VisitImage(Image img);
}
// A whole new analysis = a whole new class. No glyph is touched.
public sealed class SpellCheckVisitor : IGlyphVisitor
{
// A HashSet, not a Dictionary<K,V>: case-insensitive membership tests.
private static readonly HashSet<string> _dictionary =
new(StringComparer.OrdinalIgnoreCase) { "hello", "world", "the", "cat" };
private readonly System.Text.StringBuilder _word = new();
public readonly List<string> Misspelled = new();
public void VisitCharacter(Character c)
{
if (char.IsLetter(c.Symbol)) _word.Append(c.Symbol);
else FinishWord();
}
public void VisitImage(Image img) => FinishWord(); // images break words
private void FinishWord()
{
if (_word.Length > 0 && !_dictionary.Contains(_word.ToString()))
Misspelled.Add(_word.ToString());
_word.Clear();
}
}
// Iterator supplies the walk; Visitor supplies the operation.
var spell = new SpellCheckVisitor();
foreach (var g in GlyphTree.PreOrder(document))
g.Accept(spell);
Console.WriteLine($"Misspelled: {string.Join(", ", spell.Misspelled)}");
// Add hyphenation later: write HyphenationVisitor, change NOTHING else.
Visitor makes adding operations easy but adding element types hard: a new
Table glyph forces a new VisitTable method on every visitor. So Visitor pays off
when the element hierarchy is stable but the operations keep growing — exactly the document-analysis
situation. If your elements churn more than your operations, don't use Visitor.
IEnumerator<T> and foreach are the Iterator pattern — you use it
daily. The Roslyn compiler and System.Linq.Expressions both expose
ExpressionVisitor to walk and rewrite syntax/expression trees.
The whole system, together
Nine requirements, nine pattern decisions — and none of them stands alone. Here they are cooperating around
the Document core. Notice the colours: creational patterns
make things, structural patterns compose them,
behavioral patterns move behaviour between them.
Document/IGlyph core — that shared interface is the glue that lets nine patterns
cooperate. Green = creational, blue = structural, purple = behavioral.Patterns fuse through shared abstractions. Composite, Decorator and Flyweight all speak
IGlyph; Command and Memento both revolve around Document; Visitor and Iterator
both traverse the same tree. Design the core interface well and the patterns snap onto it like
accessories — design it badly and every pattern needs an adapter to reach the next.
Interactive: the editor design explorer
Click any component of the editor's architecture. It highlights, and the panel tells you which pattern it uses and why that requirement led there. This is the "reason to the pattern" muscle you're building.
And a tiny working editor, to feel three of the patterns run at once — type text (Command), toggle a border (Decorator), and undo down the command history (Command + Memento):
Pattern collaboration map
Beyond this one editor, certain pattern pairings recur across almost every system. Learn them as idioms — when you see one, the other is often nearby.
| Pairing | How they collaborate | Seen in our editor? |
|---|---|---|
| Composite + Iterator + Visitor | Composite builds the tree; Iterator walks it; Visitor performs pluggable operations during the walk. | Yes — document tree, traversal, spell-check. |
| Composite + Decorator | They share one component interface on purpose, so decorators nest into the composite tree like any node. | Yes — borders/scrollbars wrap glyphs. |
| Composite + Flyweight | The tree holds lightweight nodes that reference shared flyweights for their heavy, repeated data. | Yes — glyph tree + shared characters. |
| Command + Memento | A command snapshots pre-execution state as a memento so it can undo actions with no easy inverse. | Yes — Format command's undo. |
| Abstract Factory + Singleton | Usually one factory instance exists app-wide; it's held as a Singleton (or a single DI registration). | Yes — one GUI factory chosen at startup. |
| Abstract Factory + Factory Method | Each create-method of the abstract factory is frequently a Factory Method in the concrete factory. | Implicitly. |
| Bridge + Abstract Factory | The factory can create and pair the right implementor with the abstraction (look × platform). | Yes — widgets over window systems. |
| Builder + Composite | Builder's job is often precisely to assemble a Composite tree step by step. | Yes — DocumentBuilder builds glyphs. |
| Strategy + Template Method | A template method fixes the skeleton; strategies fill the varying step (here, line-breaking). | Compositor as a strategy. |
Check yourself
Each question is a mini design brief. Reason from the requirement to the pattern — that's the whole skill of this chapter.
Confused which pattern is which? Chapter 12 — Confusable Patterns lines up the tricky look-alikes (Decorator vs Proxy vs Adapter, Strategy vs State, Factory Method vs Abstract Factory) side by side. Then Chapter 13 tests the whole module with scenario questions like these.