3.0 Design-Level Structural Patterns
Design-level structural patterns address how objects are composed to form larger,
more capable objects. The three patterns here - Decorator, Adapter, and Composite -
each handle a distinct composition problem:
- Decorator adds behavior without subclassing.
- Adapter bridges two incompatible interfaces.
- Composite lets a tree of objects be treated as a single object.
These are design-level patterns, operating on individual objects and classes.
They are distinct from the architectural structural patterns - monolithic, data flow,
factored - covered in SWDevStory: Structural Patterns.
3.1 Decorator
Intent: attach additional behavior to an object dynamically, without
modifying its class and without using subclassing. Decorators are stackable and
composable.
A Decorator wraps a Component and implements the same interface. Its operation
method adds behavior before or after delegating to the wrapped component.
Multiple decorators can be stacked, each extending the behavior of the one beneath it.
classDiagram
class Writer {
<<interface>>
+write(data)
}
class FileWriter {
-file : File
+write(data)
}
class BufferedWriter {
-inner : Box~dyn Writer~
-buf : Vec~u8~
+write(data)
}
class GzipWriter {
-inner : Box~dyn Writer~
-encoder : GzEncoder
+write(data)
}
class CrcWriter {
-inner : Box~dyn Writer~
-crc : u32
+write(data)
}
Writer <|.. FileWriter : implements
Writer <|.. BufferedWriter : implements
Writer <|.. GzipWriter : implements
Writer <|.. CrcWriter : implements
BufferedWriter o--> Writer : wraps
GzipWriter o--> Writer : wraps
CrcWriter o--> Writer : wraps
interface Writer {
fn write(&mut self, data: &[u8]) -> Result
}
struct FileWriter { file: File }
struct BufferedWriter { inner: Box<dyn Writer>, buf: Vec<u8> }
struct GzipWriter { inner: Box<dyn Writer>, encoder: GzEncoder }
struct CrcWriter { inner: Box<dyn Writer>, crc: u32 }
impl Writer for BufferedWriter {
fn write(&mut self, data: &[u8]) -> Result {
self.buf.extend_from_slice(data);
if self.buf.len() >= FLUSH_SIZE { self.flush() }
Ok(())
}
}
// stack decorators at construction:
let w = CrcWriter::new(GzipWriter::new(BufferedWriter::new(FileWriter::new(path))));
The caller writes to w and gets CRC verification, gzip compression,
and buffering transparently. To add encryption, wrap with another decorator -
no existing code changes.
Decorator - Language Idioms
| Language | Idiomatic form |
| Rust |
Wrapper structs implementing the same trait; generic wrappers using
impl<W: Writer> Writer for Buffered<W> enable
zero-cost decoration with static dispatch.
|
| C++ |
Wrapper class holding std::unique_ptr<Component>;
template-based CRTP decoration for compile-time stacking without virtual
dispatch.
|
| C# |
Wrapper class implementing the interface; extension methods add decoration
at the call site without wrapping objects; Stream in the BCL
is the canonical Decorator example.
|
| Python |
The @decorator function syntax is a direct language feature.
functools.wraps preserves the wrapped function's metadata.
Class decorators wrap entire classes.
|
Decorator - Pros and Cons
| Notes |
| Pros |
Behavior is added without modifying existing classes; decorators are
independently composable; each decorator has a single responsibility;
the set of features can be varied at construction time.
|
| Cons |
Deeply nested wrappers are harder to debug; the order of stacking matters
and is the caller's responsibility; identity checks (is-a) fail when a
concrete type is wrapped.
|
| Best for |
I/O streams, middleware pipelines, cross-cutting concerns like logging and
metrics, and any situation where a single base behavior needs to be extended
in multiple optional and combinable ways.
|
3.2 Adapter
Intent: convert the interface of an existing class into the interface
that the client expects. An adapter bridges two incompatible interfaces without
modifying either.
The client calls the Target interface. The Adapter implements Target by translating
each Target method call into one or more calls on the Adaptee. Neither the client
nor the adaptee changes.
classDiagram
class Logger {
<<interface>>
+log(level, msg)
}
class SyslogWriter {
+write_entry(priority, text)
}
class SyslogAdapter {
-inner : SyslogWriter
+log(level, msg)
}
Logger <|.. SyslogAdapter : implements
SyslogAdapter o--> SyslogWriter : adapts
// Client expects this interface:
interface Logger {
fn log(&self, level: Level, msg: &str)
}
// Existing third-party library provides:
struct SyslogWriter {
fn write_entry(&self, priority: u8, text: &str)
}
// Adapter:
struct SyslogAdapter { inner: SyslogWriter }
impl Logger for SyslogAdapter {
fn log(&self, level: Level, msg: &str) {
let priority = match level {
Level::Error => 3,
Level::Warn => 4,
Level::Info => 6,
Level::Debug => 7,
};
self.inner.write_entry(priority, msg);
}
}
The rest of the application works with Logger and never imports
SyslogWriter. Replacing the logging backend means replacing the adapter,
not touching application code.
Adapter - Language Idioms
| Language | Idiomatic form |
| Rust |
The newtype pattern: a tuple struct wrapping the adaptee, implementing
the target trait. The orphan rule sometimes forces the newtype when
implementing a foreign trait for a foreign type.
|
| C++ |
Wrapper class (object adapter) or multiple inheritance (class adapter).
Object adapter is preferred - it works when the adaptee class is not
available for subclassing.
|
| C# |
Wrapper class implementing the target interface, delegating to the adaptee
held as a field. Extension methods can add the target interface to a
third-party type without a wrapper class in simple cases.
|
| Python |
A wrapper class or a thin function that calls the adaptee with translated
parameters. Duck typing reduces the need for formal adapters when the
signature difference is minor.
|
Adapter - Pros and Cons
| Notes |
| Pros |
Integrates legacy or third-party code without modifying it; isolates the
incompatibility in one place; the client and adaptee remain decoupled.
|
| Cons |
The adapter is additional code to maintain; when both interfaces change
frequently the adapter requires corresponding updates; deep translation
(semantic mismatch, not just naming) is harder to hide cleanly.
|
| Best for |
Wrapping third-party libraries to match your internal interfaces, migrating
from one library to another incrementally, and test doubles that adapt a
mock to a production interface.
|
3.3 Composite
Intent: compose objects into tree structures and treat individual
objects (leaves) and compositions (composites) uniformly through a common interface.
Recursive operations on a composite tree work without the caller knowing whether
it holds a leaf or a subtree.
classDiagram
class FsEntry {
<<interface>>
+name() str
+size() u64
+print(indent)
}
class File {
-name : String
-size_bytes : u64
+name() str
+size() u64
+print(indent)
}
class Directory {
-name : String
-children : Vec~Box~dyn FsEntry~~
+name() str
+size() u64
+print(indent)
}
FsEntry <|.. File : implements
FsEntry <|.. Directory : implements
Directory o--> "0..*" FsEntry : contains
interface FsEntry {
fn name(&self) -> &str
fn size(&self) -> u64
fn print(&self, indent: usize)
}
struct File {
name: String,
size_bytes: u64,
}
struct Directory {
name: String,
children: Vec<Box<dyn FsEntry>>,
}
impl FsEntry for File {
fn size(&self) -> u64 { self.size_bytes }
fn print(&self, indent: usize) { println!("{:>indent$}{}", "", self.name) }
}
impl FsEntry for Directory {
fn size(&self) -> u64 { self.children.iter().map(|c| c.size()).sum() }
fn print(&self, indent: usize) {
println!("{:>indent$}{}/", "", self.name);
for child in &self.children { child.print(indent + 2); }
}
}
Calling root.size() recurses the entire tree without any explicit
traversal code in the caller. Adding a new kind of entry - a symbolic link, a
compressed archive - requires only a new type implementing FsEntry.
Composite - Language Idioms
| Language | Idiomatic form |
| Rust |
Recursive enum: enum Node { Leaf(Data), Branch(Vec<Node>) }
for closed trees where all variants are known at compile time.
Box<dyn Trait> for open trees where new leaf types are
added independently.
|
| C++ |
Abstract base class with virtual size() and virtual child
operations; std::unique_ptr<Component> in the children
vector. Define child operations only on Composite, not on the base class,
to keep the leaf interface minimal.
|
| C# |
Interface with default method implementations (C# 8+) or abstract base
class; IEnumerable<IComponent> on the composite. The
TreeView control in WinForms/WPF is a direct application of this pattern.
|
| Python |
A base class with __iter__ returning children; duck typing
means any object with the right methods qualifies. The built-in
pathlib.Path walks a composite tree without formalizing the pattern.
|
Composite - Pros and Cons
| Notes |
| Pros |
Recursive operations on trees require no traversal code in callers; new leaf
types integrate without changing existing code; the interface is uniform -
callers need not distinguish leaves from composites.
|
| Cons |
Placing child-management methods on the component interface makes them
meaningless for leaves; enforcing restrictions (only certain types as
children) is harder when the interface is fully uniform.
|
| Best for |
File system traversal, UI widget hierarchies, parse trees, scene graphs,
and any domain where part-whole hierarchies occur and uniform treatment
of leaves and subtrees simplifies the calling code.
|
3.4 References
Structural Patterns References