Structural Design Patterns
Creational patterns (Chapter 3) were about making objects. Structural patterns are about composing them — how classes and objects fit together into larger structures that stay flexible, so you can add capabilities or swap parts without rewriting everything. Seven patterns, one big idea: prefer wiring small objects together over building one giant rigid class.
The big idea
A structural pattern answers the question: "I have several classes/objects — how do I arrange them so the whole is flexible?" The Gang of Four split these into two flavours:
- Structural class patterns use inheritance to compose interfaces or implementations (one class inherits from several). Static — fixed at compile time.
- Structural object patterns compose objects — one object holds a reference to another and delegates work to it. The killer advantage: you can change the composition at run-time, which pure inheritance can never do.
Almost every structural pattern here is a specific, disciplined way of applying two principles you met in Chapter 2: composition over inheritance (hold an object instead of subclassing) and the Open/Closed Principle (extend by adding new small classes, not by editing existing ones). If those two ideas feel solid, these patterns are just named recipes for them.
Jump to a pattern:
Make an incompatible interface fit. Bridge
Split two dimensions of variation apart. Composite
Treat a tree of parts uniformly. Decorator
Add behaviour by wrapping. Facade
One simple door to a complex subsystem. Flyweight
Share data across millions of objects. Proxy
A stand-in that controls access.
Five of these patterns — Adapter, Bridge, Decorator, Proxy, Facade — all work by holding another object and forwarding calls to it. So they can look identical in code. What separates them is intent, not shape. We call that out for each pattern, and there's a full disambiguation at the end of this page and in Chapter 12.
The seven structural patterns
Adapter structural a.k.a. Wrapper
Intent — one line: convert the interface a class has into the interface a client expects, so two things that were never designed to work together suddenly can.
You fly from Sri Lanka to the UK with a phone charger that has the wrong plug. The wall socket and your plug both do their job perfectly — they just don't fit. A travel adapter sits between them: it exposes the shape the UK socket expects on one side, and accepts your plug on the other. Neither the socket nor your charger changes. That's exactly the pattern.
The problem
You're building a stock-market monitoring app. It downloads quotes in XML and draws charts. Later you buy a slick third-party analytics library to spot trends — but it only accepts JSON. You can't edit the library: you don't have its source, and changing it would break everyone else who uses it. Your XML data and the library simply speak different languages.
// The analytics library, which you cannot change:
public class SmartAnalytics
{
public string Analyze(IJsonSource source) // wants JSON
=> $"Trend for {source.GetJson()}";
}
// Your existing data source speaks XML:
public class XmlStockFeed
{
public string GetXml() => "<stock price='142.5'/>";
}
var feed = new XmlStockFeed();
var analytics = new SmartAnalytics();
analytics.Analyze(feed); // ✖ won't compile:
// XmlStockFeed is not an IJsonSource
// One small class that IS a JSON source
// but WRAPS the XML feed and translates.
public class XmlToJsonAdapter : IJsonSource
{
private readonly XmlStockFeed _feed;
public XmlToJsonAdapter(XmlStockFeed feed) => _feed = feed;
public string GetJson()
{
string xml = _feed.GetXml(); // ask adaptee
// ...real code parses XML here...
return "{ \"price\": 142.5 }"; // hand back JSON
}
}
var adapter = new XmlToJsonAdapter(new XmlStockFeed());
analytics.Analyze(adapter); // ✔ library is happy
An adapter implements the interface the client wants and holds a reference to the object that has the wrong interface, translating every call between them.
Solution — the full cast
Four roles: the Target (interface the client wants — IJsonSource), the
Adaptee (the existing, incompatible class — XmlStockFeed), the
Adapter (translator), and the Client (SmartAnalytics). Now — there
are two ways to build the adapter, and the slides call this out explicitly.
The adapter holds an adaptee and delegates to it. This is the version above, and the one you should reach for by default — it's the "object" flavour of the pattern.
public interface IJsonSource // Target
{
string GetJson();
}
public class XmlStockFeed // Adaptee (unchangeable)
{
public string GetXml() => "<stock price='142.5'/>";
}
public class XmlToJsonAdapter : IJsonSource // Object adapter
{
private readonly XmlStockFeed _feed; // ← composition
public XmlToJsonAdapter(XmlStockFeed feed) => _feed = feed;
public string GetJson() => Translate(_feed.GetXml());
private static string Translate(string xml) => "{ \"price\": 142.5 }";
}
Because it holds the adaptee by reference, one object adapter can adapt an adaptee and all its subclasses, and you can even swap the adaptee at run-time.
The GoF "class adapter" inherits from both the target and the adaptee at once — so it needs multiple inheritance of classes, which only languages like C++ allow.
C# forbids inheriting from two classes. But — because the Target is an interface, C# gives you a legitimate hybrid: inherit the adaptee class and implement the target interface. You get the adaptee's methods "for free" (no wrapping field), which is the spirit of a class adapter.
// Hybrid "class adapter": inherit the adaptee, implement the target.
public class XmlToJsonClassAdapter : XmlStockFeed, IJsonSource
{
// GetXml() is inherited directly — no field, no delegation object.
public string GetJson() => Translate(GetXml());
private static string Translate(string xml) => "{ \"price\": 142.5 }";
}
Downside: you're now permanently bound to XmlStockFeed as a base class, and you can't adapt its
subclasses selectively. That rigidity is exactly why the object adapter is usually the better call.
When to use it — and the costs
- You want to use an existing class but its interface doesn't match what you need.
- You're integrating a third-party / legacy library you can't modify.
- You need several existing subclasses to cooperate and subclassing each one is impractical (use an object adapter — it adapts the parent and its children).
- More classes and one extra layer of indirection.
- Translation can be lossy or slow (parsing XML→JSON on every call).
- If you control both sides, you probably don't need an adapter — just fix the interface.
StreamReader / StreamWriter adapt a raw byte Stream into a
character-based TextReader / TextWriter. The Encoding classes wrap and
adapt between byte[] and string. Older still, System.Data's
DataAdapter literally has "Adapter" in its name — it bridges a database and an in-memory
DataSet.
Adapter vs Bridge: the deciding difference is intent — an adapter converts one
existing interface into another so mismatched classes cooperate, while Bridge splits an abstraction from its
implementation so the two dimensions vary independently. A handy clue (not a definition): adapters usually appear
after the fact around code you can't change, and a bridge is typically designed in up front — but a
bridge introduced later, or an adapter planned early, doesn't stop being itself.
Adapter vs Decorator: a decorator keeps the same interface and adds behaviour; an
adapter gives a new interface and (usually) adds nothing.
Adapter vs Proxy/Facade: a proxy keeps the same interface but controls access; a facade invents
a new, simpler interface over many objects. Full side-by-sides in
Chapter 12.
Bridge structural
Intent — one line: when a class varies in two independent directions at once, split it into two separate hierarchies — an abstraction and an implementation — and connect them with a reference (the "bridge"), so each direction grows on its own.
Bridge trips people up because the code looks just like an object adapter, and the words "abstraction" and "implementation" sound vague. The whole pattern is really about one combinatorial problem. Nail that problem and the rest is easy. Let's build up to it.
A remote control and a device. A remote (basic, advanced, voice) is the abstraction — the high-level controls. A device (TV, radio, speaker) is the implementation — the thing that actually changes volume or channel. Any remote can drive any device, because they meet at a thin common interface. You can invent a new remote without touching any device, and add a new device without touching any remote. Same idea as a GUI (abstraction) sitting on top of different operating-system APIs (implementations), exactly as the slides describe.
The problem — combinatorial explosion
Say you have a Shape class. You add two shapes: Circle and Square. Fine.
Now the shapes need colours. The tempting move is to subclass:
public abstract class Shape { }
public class RedCircle : Shape { }
public class BlueCircle : Shape { }
public class RedSquare : Shape { }
public class BlueSquare : Shape { }
// 2 shapes × 2 colours = 4 classes
public interface IColor { string Fill(); } // implementation side
public class Red : IColor { public string Fill() => "red"; }
public class Blue : IColor { public string Fill() => "blue"; }
public abstract class Shape // abstraction side
{
protected readonly IColor color; // ← the bridge
protected Shape(IColor color) => this.color = color;
public abstract string Draw();
}
public class Circle : Shape
{
public Circle(IColor c) : base(c) { }
public override string Draw() => $"Circle filled {color.Fill()}";
}
new Circle(new Blue()).Bridge = "convert inheritance into composition." Instead of one class hierarchy that tries to capture every combination of two traits, keep two small hierarchies and let one hold a reference to the other. M×N collapses to M+N.
Deep dive: the slides' device × remote example in C#
Here the Remote is the abstraction (high-level control logic) and the Device
is the implementation (low-level work). A BasicRemote works with any device through the thin
IDevice interface — and you could add an AdvancedRemote (refined abstraction) without
touching a single device.
public interface IDevice // Implementation
{
int Volume { get; set; }
string Name { get; }
}
public class Radio : IDevice { public int Volume { get; set; } public string Name => "Radio"; }
public class Tv : IDevice { public int Volume { get; set; } public string Name => "TV"; }
public class BasicRemote // Abstraction (holds an IDevice = the bridge)
{
protected readonly IDevice device;
public BasicRemote(IDevice device) => this.device = device;
public void VolumeUp() { device.Volume++; Console.WriteLine($"{device.Name}: {device.Volume}"); }
public void VolumeDown() { device.Volume--; Console.WriteLine($"{device.Name}: {device.Volume}"); }
}
public class AdvancedRemote : BasicRemote // Refined abstraction — new control logic, same devices
{
public AdvancedRemote(IDevice device) : base(device) { }
public void Mute() { device.Volume = 0; Console.WriteLine($"{device.Name}: muted"); }
}
// Client links an abstraction to an implementation:
var remote = new AdvancedRemote(new Tv());
remote.VolumeUp();
remote.Mute();
Notice the client's job (as the slides stress): it links the abstraction object with one of the implementation objects.
- A class has (or will have) two or more orthogonal dimensions of variation.
- You want to avoid a permanent compile-time binding between an abstraction and its implementation.
- Changes to an implementation must not ripple out to clients.
- Several objects should share one implementation, hidden from the client.
- Up-front complexity: two hierarchies and an indirection where beginners see none needed.
- Overkill if there's genuinely only one dimension of change — don't pre-split "just in case."
System.Data.Common: the ADO.NET provider model is a bridge — your data-access abstraction
(DbConnection, DbCommand) is bridged to concrete providers (SQL Server, SQLite,
PostgreSQL). Logging abstractions (ILogger) bridged to sinks (console, file, Seq) are the same idea.
Bridge vs Adapter is the classic mix-up. Same code shape (one object holds another), different intent: Adapter converts a mismatched interface so two existing classes cooperate; Bridge separates an abstraction from its implementation so two dimensions can evolve independently. Timing is only a clue — adapters commonly show up after the fact and bridges are usually planned up front, but that's a tendency, not the definition. If you're solving a "these two don't fit" problem → Adapter. If you're solving a "this class explodes in two directions" problem → Bridge. See Chapter 12.
Composite structural a.k.a. Object Tree
Intent — one line: arrange objects into a tree and let the client treat a single object (a leaf) and a whole group of objects (a composite) through the same interface.
An army. A squad contains soldiers; a platoon contains squads; a brigade contains platoons. An order given at the top ("advance") passes down the hierarchy until every soldier acts — and whoever gives the order doesn't care whether they're commanding one soldier or a whole division. Same interface at every level. (Files and folders on your computer are the same tree.)
The problem
You're building an ordering system. An order holds products, but also boxes — and a box can hold products and other boxes, nested arbitrarily. How do you compute the total price?
decimal TotalPrice(object item)
{
if (item is Product p) return p.Price;
if (item is Box b)
{
decimal sum = 0;
foreach (var child in b.Items)
sum += TotalPrice(child); // recurse, re-checking types
return sum;
}
throw new InvalidOperationException("unknown item");
}
if (item is ...). The client
must know every concrete type and how they nest. Brittle.public interface IOrderComponent { decimal GetPrice(); }
public class Product : IOrderComponent // Leaf
{
public decimal Price { get; init; }
public decimal GetPrice() => Price;
}
public class Box : IOrderComponent // Composite
{
private readonly List<IOrderComponent> _items = new();
public void Add(IOrderComponent c) => _items.Add(c);
public decimal GetPrice() => _items.Sum(c => c.GetPrice()); // delegates down
}
order.GetPrice(). A box asks each child for
its price — and a child might be a product or another box; it doesn't matter. New leaf types plug in
with zero changes to traversal code.Give leaves and containers a common interface. A container implements each operation by delegating to its children and combining their results — so recursion lives in the structure, not in the client.
The three roles: Component (the shared interface — IOrderComponent),
Leaf (no children, does the real work — Product), and Composite /
Container (has children, delegates — Box). Here it is as a file system, which we'll drive in the
demo:
public interface IFileNode
{
string Name { get; }
long GetSize(); // both leaves and folders answer this
}
public class FileLeaf : IFileNode // Leaf
{
public string Name { get; }
private readonly long _size;
public FileLeaf(string name, long size) { Name = name; _size = size; }
public long GetSize() => _size;
}
public class Folder : IFileNode // Composite
{
public string Name { get; }
private readonly List<IFileNode> _children = new();
public Folder(string name) { Name = name; }
public void Add(IFileNode node) => _children.Add(node);
public long GetSize() => _children.Sum(c => c.GetSize()); // recurse
}
project.GetSize() asks each child, and folders recurse — the client never type-checks.GetSize() call works on
a single file and on a folder nested three levels deep.- Your core model is naturally a tree (files, org charts, UI widgets, menus, ASTs).
- You want clients to ignore the difference between individual objects and groups of them.
- The shared interface can get "too general" — leaves may be forced to expose
Add/Removethat make no sense for them (a design tension GoF openly acknowledges). - Type-safety loosens: everything is an
IComponent.
WPF / WinForms controls: a Panel contains controls, which may be panels containing more
controls — and layout/rendering walk the tree uniformly. The Roslyn syntax tree and any XML/JSON
DOM (XElement with child XElements) are composites too.
Composite vs Decorator: both build a tree of same-interface objects, but a decorator wraps exactly one child to add behaviour; a composite holds many children to aggregate them. GoF: "a decorator is a degenerate composite with only one component." Composite pairs naturally with Iterator (to traverse) and Visitor (to add operations). More in Chapter 12.
Decorator structural a.k.a. Wrapper
Intent — one line: add responsibilities to an individual object at run-time by wrapping it in another object that shares its interface — as many layers deep as you like.
Getting dressed. Cold? Add a sweater. Still cold? A jacket over it. Raining? A raincoat on top. Each garment "extends" you without becoming part of you, and you can peel any layer off whenever you like. Each layer wraps the ones beneath and adds something — exactly what a decorator does.
The problem
A coffee shop sells an espresso, and customers add milk, sugar, whipped cream, oat milk… in any combination. You need a price and a description for each order. The obvious approach — a subclass per combination — detonates.
public class Espresso { }
public class EspressoWithMilk { }
public class EspressoWithMilkAndSugar { }
public class EspressoWithMilkSugarWhip { }
public class EspressoWithOatMilk { }
// ...4 add-ons → up to 2⁴ = 16 classes,
// and every new add-on DOUBLES the count.
IBeverage order = new Espresso(); // 2.50
order = new Milk(order); // +0.50
order = new Sugar(order); // +0.25
order = new Whip(order); // +0.75
Console.WriteLine($"{order.Description()} = {order.Cost():C}");
// Espresso, milk, sugar, whip = $4.00
A decorator implements the same interface as the thing it wraps and holds a reference to it — so it can be used wherever the original is used, and it typically forwards each call to the inner object while adding a little something before or after. Forwarding is the common case, not a law: a decorator may short-circuit — a caching decorator, for instance, can return a stored result without calling the wrapped object at all.
public interface IBeverage // Component
{
string Description();
decimal Cost();
}
public class Espresso : IBeverage // Concrete component
{
public string Description() => "Espresso";
public decimal Cost() => 2.50m;
}
// Base decorator: itself an IBeverage, WRAPS an IBeverage.
public abstract class BeverageDecorator : IBeverage
{
protected readonly IBeverage inner;
protected BeverageDecorator(IBeverage inner) => this.inner = inner;
public virtual string Description() => inner.Description();
public virtual decimal Cost() => inner.Cost();
}
public class Milk : BeverageDecorator
{
public Milk(IBeverage b) : base(b) { }
public override string Description() => inner.Description() + ", milk";
public override decimal Cost() => inner.Cost() + 0.50m;
}
public class Sugar : BeverageDecorator
{
public Sugar(IBeverage b) : base(b) { }
public override string Description() => inner.Description() + ", sugar";
public override decimal Cost() => inner.Cost() + 0.25m;
}
// Whip, OatMilk, Caramel… all follow the identical shape.
Inheritance is decided at compile time; decoration is decided at run-time. With subclasses you must know every combination before you build. With decorators you compose combinations while the program runs — from user input, config, data — and you can even wrap the same type twice (double sugar). This is composition over inheritance made concrete.
Cost() on the outer Whip calls Sugar → Milk → Espresso, and each adds
its charge on the way back. The whole stack is still just an IBeverage.Deep dive: the canonical .NET example — the Stream family
The .NET I/O stack is textbook Decorator. Every wrapper is a Stream and holds a
Stream, adding one capability. You compose behaviour by nesting:
// Read → Decrypt → Decompress, all through one Stream interface:
using FileStream file = File.OpenRead("data.bin"); // Concrete component
using var decrypt = new CryptoStream(file, decryptor, CryptoStreamMode.Read);
using var decompress = new GZipStream(decrypt, CompressionMode.Decompress);
using var buffered = new BufferedStream(decompress); // adds buffering
// The consumer just sees a Stream — it has no idea it's 4 layers deep.
using var reader = new StreamReader(buffered);
string text = reader.ReadToEnd();
This mirrors the slides' example exactly (encrypt + compress decorators around a data source). Each layer is independent and reorderable — the essence of Decorator.
- You need to add responsibilities to individual objects dynamically and transparently.
- Extension by subclassing would cause a combinatorial explosion of classes.
- You can't or shouldn't subclass (e.g. a sealed class, or behaviour chosen at run-time).
- Lots of tiny objects; a deep stack is harder to debug ("why is this so slow?" — 6 wrappers).
- Order can matter (encrypt-then-compress ≠ compress-then-encrypt).
- A decorator isn't identical to its component — reference equality and type checks can surprise you.
Decorator vs Adapter: a decorator keeps the same interface and adds behaviour; an adapter changes the interface. Decorator vs Proxy: nearly identical code — but a decorator enhances the object, while a proxy controls access to it (lazy-loading, permissions). Decorator vs Strategy: a decorator changes an object's "skin" (wrapping from outside); a strategy changes its "guts" (a swapped-in inner algorithm). See Chapter 12.
Facade structural
Intent — one line: put a single, simplified interface in front of a complicated subsystem, so clients push one button instead of wiring a dozen parts together in the right order.
You phone a shop to place an order. The operator is your facade: behind that one friendly voice sit the inventory system, payment gateway, and delivery service. You say "one large pepperoni to this address," and they orchestrate all of it. You never talk to the warehouse directly.
The problem
To make a pizza, six subsystems must run in the right order: prepare dough, add sauce, add toppings, bake in the oven, take payment, dispatch delivery. If every client does this by hand, your business logic gets welded to the internals of six classes.
var dough = new DoughStation();
var sauce = new SauceStation();
var oven = new Oven();
var payment = new PaymentGateway();
var courier = new DeliveryService();
var pizza = dough.Prepare("thin");
sauce.Apply(pizza, "tomato");
oven.Preheat(220); // forget this → raw pizza
oven.Bake(pizza, minutes: 12);
payment.Charge(customer, 14.99m); // wrong order → deliver unpaid
courier.Dispatch(pizza, address);
public class PizzaShopFacade
{
private readonly DoughStation _dough = new();
private readonly SauceStation _sauce = new();
private readonly Oven _oven = new();
private readonly PaymentGateway _payment = new();
private readonly DeliveryService _courier = new();
public void OrderPizza(string kind, Customer c, string addr)
{
var pizza = _dough.Prepare("thin");
_sauce.Apply(pizza, "tomato");
_oven.Preheat(220);
_oven.Bake(pizza, 12);
_payment.Charge(c, 14.99m);
_courier.Dispatch(pizza, addr);
}
}
new PizzaShopFacade().OrderPizza("pepperoni", customer, address);
A facade doesn't add features and doesn't hide the subsystem (you can still reach in when you need to) — it just offers a convenient default path for the 90% case, decoupling clients from the subsystem's moving parts.
- You want a simple entry point to a complex or sprawling subsystem/library.
- There are many dependencies between clients and a subsystem's internal classes.
- You want to layer your system — a facade per layer, talking only to the layer below.
- A facade can become a "god object" that knows too much if you keep piling features on.
- It may hide power users need — so keep the subsystem accessible, don't seal it off.
HttpClient is a facade over sockets, DNS, connection pools, TLS, and message handlers.
File.ReadAllText(path) is a facade over opening a stream, wrapping a reader, reading, and disposing.
ASP.NET Core's WebApplication.CreateBuilder(args) hides a mountain of hosting/DI/config setup behind
a couple of calls.
Facade vs Adapter: an adapter makes one existing interface fit another; a facade invents a brand-new, simpler interface over many objects. Facade vs Mediator: a facade is one-directional (client → subsystem, and subsystem classes don't know it exists); a Mediator coordinates two-way chatter between peers that all know the mediator. Facades are often Singletons.
Flyweight structural
Intent — one line: when you need a huge number of similar objects, share the parts they have in common instead of duplicating them, so memory stays small. Caching is one implementation mechanism a flyweight factory happens to use — it isn't what the pattern is named for or defined by.
Intrinsic state is the data that is the same across many objects and never changes
per object — the tree's species name, its mesh, its texture bitmap. It lives inside the shared flyweight
and is read-only.
Extrinsic state is the data that is unique per object and often changes from outside —
this tree's (x, y) position, its height, its current sway. The flyweight does not store it;
the caller supplies it (passes it into methods, or stores it in a lightweight context object).
The whole pattern is: pull the extrinsic state out, keep only the intrinsic state, and share.
The problem
You're rendering a forest with a million trees. Each Tree stores its position and its
species name, colour, and a big texture/mesh. But there are only three species — so you're storing the same heavy
texture a million times.
public class Tree // ~1 KB each (texture + mesh inline)
{
public int X, Y; // unique (extrinsic)
public string Species; // repeated (intrinsic)
public string Color; // repeated
public byte[] Texture; // repeated — the expensive part!
}
var forest = new List<Tree>();
for (int i = 0; i < 1_000_000; i++)
forest.Add(new Tree { X = rx(), Y = ry(),
Species = "Oak", Color = "green", Texture = LoadOakTexture() });
// 1,000,000 copies of the SAME oak texture in RAM.
// Intrinsic state — created once per species, shared by all.
public class TreeType // Flyweight
{
private readonly byte[] _texture; // private defensive copy — never handed out raw
public string Species { get; }
public string Color { get; }
// Any holder of a raw byte[] could mutate it and corrupt every tree that
// shares this flyweight, so we only ever expose a read-only view.
public ReadOnlyMemory<byte> Texture => _texture;
public TreeType(string s, string c, byte[] t) => (Species, Color, _texture) = (s, c, (byte[])t.Clone());
public void Draw(int x, int y) => Console.WriteLine($"Drawing {Species} at ({x},{y}) with shared texture");
}
public class Tree // ~32 bytes: 16-byte header + 2 ints + a reference
{
public int X, Y; // extrinsic
private readonly TreeType _type; // shared reference
public Tree(int x, int y, TreeType type) => (X, Y, _type) = (x, y, type);
public void Draw() => _type.Draw(X, Y);
}
TreeType objects total; each of the
million trees is a tiny record holding coordinates and a reference. Memory drops from ~1 GB to roughly tens of
MB — still a dramatic win, just not literally "a few MB."The Flyweight Factory guarantees sharing: it hands back an existing flyweight if one already exists for that intrinsic state, and only creates a new one otherwise. Texture is intrinsic too, so it belongs inside the cache key's inputs — the factory loads it itself from the species, rather than accepting it as a separate parameter that could vary independently and silently break sharing.
public class TreeFactory
{
private readonly Dictionary<string, TreeType> _cache = new();
public TreeType GetTreeType(string species, string color)
{
string key = $"{species}:{color}";
if (!_cache.TryGetValue(key, out var type))
_cache[key] = type = new TreeType(species, color, LoadTexture(species)); // create once
return type; // otherwise share
}
private static byte[] LoadTexture(string species) => /* load from disk/atlas */ new byte[1024];
}
- An app holds a very large number of objects and memory is the bottleneck.
- Most of each object's state can be made extrinsic (pulled out and passed in).
- Many groups of objects collapse to a few shared ones once extrinsic state is removed.
- The app doesn't rely on object identity (shared flyweights are
==).
- Code gets harder to read: extrinsic state now sloshes around in method parameters.
- You trade memory for CPU (computing/looking up extrinsic state each call).
- Flyweights must be immutable and can't know their context — no back-pointers to owners.
String interning: the runtime keeps one shared copy of each identical string literal
(string.Intern); every reference points at it. Shared Encoding.UTF8 and
Brushes.Red-style singletons are flyweights too. (Ordinary C# boxing does not do this —
two separately boxed bool/int values are distinct objects, so
ReferenceEquals is false. Java's Integer.valueOf caches small ints this way; .NET's
boxing doesn't.)
Flyweight vs Singleton: a singleton is exactly one instance of a class; a flyweight is one shared instance per distinct intrinsic state (three tree species → three flyweights). Flyweight pairs well with Composite (shared leaf nodes) and is a good way to implement State / Strategy objects. More in Chapter 12.
Proxy structural
Intent — one line: provide a stand-in with the same interface as a real object, so the stand-in can control access to it — delaying its creation, checking permissions, caching, or logging.
A credit card is a proxy for your bank account, which is a proxy for a pile of cash. All three "make a payment," but the card controls access to the money: it checks your limit, records the transaction, and saves you carrying the cash around. Same interface, extra control.
The problem
A gallery shows high-resolution images. Each RealImage loads its (large) file from disk the moment
it's created — so opening the gallery loads every image up front, even the ones the user never scrolls
to. Slow start, wasted memory.
public class RealImage : IImage
{
private readonly string _file;
public RealImage(string file)
{
_file = file;
LoadFromDisk(file); // heavy — happens immediately
}
public void Display() => Console.WriteLine($"Showing {_file}");
private void LoadFromDisk(string f) => Console.WriteLine($"Loading {f} (4 MB)...");
}
// Opening the gallery loads ALL 200 images now,
// even though only ~5 are on screen.
var gallery = files.Select(f => new RealImage(f)).ToList();
if (notLoaded) Load(); checks everywhere,
but that duplicates the same guard across every caller.public class ProxyImage : IImage // same interface as RealImage
{
private readonly string _file;
private RealImage? _real; // created on first use
public ProxyImage(string file) => _file = file;
public void Display()
{
_real ??= new RealImage(_file); // load the heavy object ONCE, on demand
_real.Display(); // then cache it for next time
}
}
// Cheap to create 200 proxies; each loads only when first shown.
var gallery = files.Select(f => new ProxyImage(f)).ToList();
IImage interface — so nothing else changes.A proxy has the same interface as the real object and holds a reference to it, but it runs some control logic (create-on-demand, permission check, cache, log) before or after forwarding the call.
| Flavour of Proxy | What it controls | Example |
|---|---|---|
| Virtual | Creation cost — lazy-init an expensive object | The image gallery above; Lazy<T> |
| Protection | Access rights — allow/deny by permission | Only admins may call Delete() |
| Remote | Location — a local stand-in for an object elsewhere | gRPC / WCF client proxy over the network |
| Caching | Repetition — reuse results of expensive calls | Memoise a slow query response |
| Logging | Observability — record every call | Audit trail around a service |
IImage. The client holds a proxy
and can't tell the difference; the proxy decides when to create and call the real object.RealImage from disk; every later view of that image is an instant cache hit. Creating the proxies
cost nothing — the expensive work is deferred until it's actually needed.- You need a smarter reference than a plain pointer: lazy creation, access control, caching, logging, or a local handle to a remote object.
- You want that control logic to be transparent — callers keep using the same interface.
- Another layer of indirection; the deferred work can cause a surprising pause "later."
- Care needed with threading (two callers triggering lazy-init at once).
Entity Framework generates lazy-loading proxy classes so a navigation property loads from the
database only when you first touch it. Lazy<T> is a virtual proxy in the box. gRPC / WCF client
stubs are remote proxies, and System.Reflection.DispatchProxy lets you build your own (great for
logging/caching interceptors).
Proxy vs Decorator: code looks the same, but a decorator adds responsibilities you choose to stack; a proxy controls access to one fixed real object (and often manages its lifecycle). Proxy vs Adapter: a proxy keeps the same interface; an adapter gives a different one. Proxy vs Facade: a facade wraps many classes behind a new, simpler interface; a proxy wraps one object behind the same interface. See Chapter 12.
Structural patterns at a glance
| Pattern | Intent in one line | You reach for it when… |
|---|---|---|
| Adapter | Make an incompatible interface fit. | You must use a class whose interface is wrong for you. |
| Bridge | Split two independent dimensions into two hierarchies. | A class varies in two directions and subclasses explode (M×N). |
| Composite | Treat a tree of parts and wholes uniformly. | Your model is a tree and clients shouldn't care about node type. |
| Decorator | Add behaviour by wrapping, at run-time. | Add-on combinations would explode into subclasses. |
| Facade | One simple door to a complex subsystem. | Clients are drowning in a subsystem's moving parts. |
| Flyweight | Share common data across many objects. | Millions of similar objects blow the memory budget. |
| Proxy | A stand-in that controls access. | You need lazy-load, access control, caching, or a remote handle. |
Adapter, Bridge, Decorator, Proxy, and Facade all hold another object and forward calls, so they're endlessly confused. Sort them by intent, not code shape: Adapter changes an interface to make things fit; Decorator keeps the interface and adds behaviour (stackable); Proxy keeps the interface and controls access (one fixed target); Facade invents a new, simpler interface over many objects; and Bridge is a designed-in split of an abstraction from its implementation so both vary freely. When two still feel the same, the deciding question is "is the interface the same, different, or brand-new — and am I adding, controlling, or simplifying?" The full side-by-side grids live in Chapter 12 · Confusable Patterns.
Check yourself
You've now seen how objects are composed into flexible structures. Next, Chapter 5 · Behavioral Patterns tackles how those objects talk to each other. And whenever two patterns blur together, the Confusable Patterns chapter has the side-by-side grids.