SWDev Patterns

SWDev Patterns: Structural

decorator, adapter, composite

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
LanguageIdiomatic 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
LanguageIdiomatic 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
LanguageIdiomatic 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
ResourceDescription
Refactoring Guru - Decorator Decorator pattern with stream wrapping examples and diagrams.
Refactoring Guru - Adapter Object and class adapter variants with cross-language examples.
Refactoring Guru - Composite Composite pattern with tree traversal examples.
SWDevStory: Structural Patterns Architectural structural patterns - monolithic, data flow, factored.