KitDesk can render the selected pickups as text or CSV. Some volunteers want a revision note alongside the export. A training session also needs a visible preview warning. These options vary independently of the export format. Creating a separate job subclass for every combination would encode a growing matrix in the class hierarchy. Decorator lets KitDesk compose those optional responsibilities around the same renderer interface.
The problem: independent options multiply combinations
Start with two export formats and two optional annotations. A subclass for text-with-revision, another for text-with-preview, another for text-with-both, and a parallel set for CSV duplicates knowledge of annotation behavior. Adding one more option multiplies combinations again. The difficulty is not the existence of subclasses; it is that independent choices are being represented as a single inheritance dimension.
The forces are independent composition, preservation of the renderer's contract and an understandable execution order. We want the client to call render(rows) regardless of which options are active. We also want the CSV payload to remain valid. In this lesson annotations belong in the artifact's notes array, not as arbitrary lines appended to its encoded text.
The shared renderer contract returns { mediaType, text, notes }. A note is plain presentation text. The export preview displays these notes beside the payload; a consumer that saves only text will not carry them along. A preview note is therefore an informative annotation, not an authorization check or a guarantee that a printed copy cannot be used.
Wrap a renderer with another renderer
class RevisionNoteRenderer {
constructor(inner, revision) {
if (!Number.isSafeInteger(revision) || revision < 0) {
throw new TypeError("revision must be a nonnegative safe integer");
}
this.inner = inner;
this.revision = revision;
}
render(rows) {
const artifact = this.inner.render(rows);
return Object.freeze({ ...artifact, notes: Object.freeze([
...artifact.notes, `Reservation snapshot revision ${this.revision}`
]) });
}
}
class PreviewNoteRenderer {
constructor(inner) { this.inner = inner; }
render(rows) {
const artifact = this.inner.render(rows);
return Object.freeze({ ...artifact, notes: Object.freeze([
...artifact.notes, "PREVIEW: not a collection record"
]) });
}
}
// Standalone base renderer; CsvExportJob from the previous lesson also fits.
const base = {
render(rows) {
return Object.freeze({
mediaType: "text/plain",
text: rows.map(row => `${row.id} ${row.kit}`).join("\n"),
notes: Object.freeze([])
});
}
};
const renderer = new PreviewNoteRenderer(new RevisionNoteRenderer(base, 7));
const artifact = renderer.render([{ id: "R32", kit: "SAW" }]);
console.log(artifact.text); // R32 SAW
console.log(artifact.notes);
// ["Reservation snapshot revision 7", "PREVIEW: not a collection record"]
The component role is the render(rows) convention. The base object is a concrete component. Each decorator implements that same convention and holds another renderer. The client sees one renderer even though the call travels through several objects. The new note array prevents a wrapper from modifying an artifact retained by its inner renderer.
Use the TextExportJob or CsvExportJob from Factory Method: let export workflows create their encoders in place of the standalone base to join the examples. Those jobs already return the same envelope. The decorator knows nothing about createEncoder() or concrete encoders, and the job knows nothing about annotations.
A primary API example of this forwarding shape is Java's FilterWriter. It extends Writer, contains another Writer and provides default forwarding behavior that subclasses can alter. Recognizing a decorator-like structure there is an interpretation of its documented contract; KitDesk's artifact notes are our own example.
Trace both entry order and return order
The object graph is client → preview decorator → revision decorator → base renderer. Calls enter in that order. The base returns its artifact first; the revision decorator appends its note; the preview decorator appends its warning. The artifact seen by the client therefore contains revision then preview. The structural diagram must show nested object references, not inheritance from a separate subclass for every combination.
Reverse the wrappers and the notes reverse too. new RevisionNoteRenderer(new PreviewNoteRenderer(base), 7) produces preview then revision. Both preserve the payload, but a UI that displays notes in array order will look different. A pattern does not make operations commutative. Pick a canonical composition order and test the user-visible result.
The revision is supplied at construction, so it is a snapshot label. If reservation data changes, construct a fresh renderer with the revision corresponding to the new row snapshot. This decorator does not inspect a mutable board or prove that arbitrary rows came from revision seven. A real application must capture rows and revision together in its application workflow; labeling the wrong snapshot is possible even with perfectly composed wrappers. When a snapshot authorizes an external write, the release-note agent's revision-bound approval policy requires an atomic check and capture, rather than trusting a presentation label.
If the inner renderer throws, these wrappers propagate the error and produce no artifact. They do not annotate a failure as if it were a successful export. Reusing the same wrapper twice appends two notes; the example does not deduplicate options. Composition code owns that decision.
Live model · Sequence view
Inspect the wrapper graph and return order
The KitDesk model shows preview and revision renderers nesting around an export job through the same render(rows) contract. Notes are artifact metadata, separate from encoded text.
Explore scenarios in this model
- Architecture view
- Prepare by deadline
- Load cabinet reservations
- Render CSV manifest
- Mark ready and notify views
- Render annotated preview
These links open the same public model in a selected view. They do not change its source.
Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.
Exploration is local to your viewer. Reload or Reset view returns to the source layout.
Trade-offs and when an options object is enough
Decorators replace a subclass matrix with an object graph. That graph can be harder to inspect in a debugger, and every layer must preserve the component contract. If one wrapper unexpectedly returns a promise or changes text into a structured object, the common method name does not make it substitutable. Keep input, output and error conventions stable.
Two simple notes may not justify two classes in production. An options object and one small rendering function are often easier to follow when the option set is closed and owned by one team. Decorator becomes more useful when optional behaviors are separately maintained, composed at runtime or reusable around several renderer implementations.
Do not use an optional decorator for a business rule that must always run. If every exported manifest must meet a required validation rule, enforce it in the export workflow. A caller that constructs the base renderer directly can bypass optional wrappers. Likewise, a preview note must not be treated as the mechanism preventing physical collection or unauthorized access.
The cabinet Adapter also wraps an object, but it translates a mismatched interface into a target interface. These decorators retain the renderer interface and add responsibilities. A Strategy chooses one algorithm for a context; decorators can stack several behaviors around one component. Name the intended change before choosing a familiar wrapper shape.
Read the complete KitDesk collaboration
The application reads normalized reservations through the adapter. A board owns their readiness and notifies local views. A preparation action captures ready data and uses a chosen ordering Strategy. An export job obtains its encoder through a Factory Method. Optional decorators annotate the returned artifact. The volunteer sees a preview and can deliberately save the encoded payload.
These are five separate variation points inside one application. They do not require five services or a mandatory five-pattern framework. The lessons isolate each pressure so you can recognize it. A real KitDesk implementation could keep its branch, inject an encoder and call its only view directly until independent variation makes the more elaborate structures worthwhile.
Live model · Architecture view
Place the wrapper chain in the complete system
The shared KitDesk architecture places manifest rendering beside reservation loading, pickup ordering and readiness notification. Preview and revision wrappers use the rendering contract; they do not acquire responsibility for collection authorization or session selection.
Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.
Practice: prove substitutability and inspect composition
- Render through the base, each wrapper alone and both wrapper orders. Check that media type and text are identical and that the exact note order matches the chosen object graph.
- Give a decorator a base renderer that returns the same frozen artifact on every call. Confirm repeated decorated renders never change the base artifact or accumulate notes across calls. Then make the inner renderer throw and verify the error propagates.
- Replace both classes with a small options-based function. Decide which version makes the current two-note requirement clearer. Explain what future independent behavior would make you reconsider.
The series ends here. A useful final exercise is to remove one pattern from the complete collaboration and explain which concrete requirement still holds and which becomes harder to satisfy. Revisit Observer: update a pickup board with explicit notification rules, return to the KitDesk learning path, or explore the Design Patterns hub.