← Design patterns
Composition Behavior around an existing interface

Decorator

Same interface, another layer.

A command-line diagnostic tool renders a few lines. Now you want line numbers and a readable frame around the result. The caller still needs the same operation: render this block.

Put those behaviors around the text block. Then follow exactly what happens when one wrapper calls another.

TypeScriptGoPythonSame output and wrapper order · across the comparison

01 / Add behavior where the operation already meets its caller

The command should not need a new way to render.

A direct render of diagnostic lines is a good start. Formatting beside that call can be enough too. The pressure appears when the terminal, a saved report, and a CI log all repeat the same surrounding behavior, or need different combinations of it.

You could build separate numbered, framed, and numbered-and-framed renderers. Their responsibilities soon overlap. Instead, let a wrapper receive a TextBlock and offer TextBlock itself. The caller can use the result wherever it used the original block.

A Decorator adds behavior around an object while preserving the interface through which its caller uses it. It holds a collaborator and delegates to it. Because the wrapper offers that same interface, another wrapper can surround it in turn.

This is the object-composition pattern, separate from the @decorator syntax in the ECMAScript decorators proposal.

Caller → TextBlock

Ask for the block.

The command handles the returned lines. It does not choose how numbering or framing is implemented.

Wrapper → inner TextBlock

Add one responsibility.

Numbers prefixes each line. Frame adds borders around the returned lines. Each wrapper still offers render.

Source → result

Return the lines.

The plain block owns the source lines and returns them when asked.

The interface remains compatible; observable behavior can change. Numbering changes each line, while framing changes the surrounding structure. The design needs a contract for those additions.

02 / Start with one wrapper, then compose two

Receive a block. Return a block.

The basic view shows the actual numbering wrapper. It delegates once, transforms the returned lines, and offers the same interface. The practical view adds framing. The plain source that both wrappers surround is in the complete form.

The source accepts at most four printable ASCII lines, each no longer than 32 characters. An empty list remains empty. The source snapshots the input so later caller mutations do not change the block.

Frame measures the lines returned by its inner block, then adds matching edges and padding.

The numbering wrapper offers TextBlock, delegates render once, and prefixes each returned line. The complete view supplies the interface and source.

TypeScriptReading
diagnostics.ts
export function withNumbers(inner: TextBlock): TextBlock {
	return {
		render() {
			return inner.render().map((line, i) => `${i + 1}: ${line}`);
		}
	};
}
GoAlongside
diagnostics.go
type numbered struct{ inner TextBlock }

func WithNumbers(inner TextBlock) TextBlock { return numbered{inner} }
func (n numbered) Render() []string {
	lines := n.inner.Render()
	result := make([]string, len(lines))
	for i, line := range lines {
		result[i] = fmt.Sprintf("%d: %s", i+1, line)
	}
	return result
}
Reading the TypeScriptStructural interfaces and a captured collaborator

Both wrapper functions return an object with render. That satisfies TextBlock without a base class. The closure retains inner and delegates to it only when the caller renders.

Numbers maps the inner lines without changing the source. Frame returns an empty list for an empty block and otherwise derives its width from the delegated result. Errors from the inner block propagate; wrappers do not silently replace a failed source.

Reading the GoAn interface field supplies the next layer

Numbered and framed values each implement Render and contain a TextBlock. The inner value is retained and called on every render, so a stateful source would remain shared.

PlainText validates before creating the source and returns an error for unsupported input. Render returns a fresh slice of strings. These wrappers do not need mutable counters, locks, or an error protocol for their pure formatting work.

Reading the PythonProtocols and explicit wrapper objects

Python’s Protocol describes the small render contract without requiring a shared base class. Each wrapper retains a TextBlock and calls its render method only when the caller renders, so a stateful source stays observable.

The implementation uses private classes because Python’s function-decorator syntax is a different language feature. The object relationship is the same: a wrapper offers the contract, delegates, and returns a transformed list.

03 / Follow the returned lines

The outer wrapper sees the inner operation.

The source starts with two diagnostic lines. With Frame outside Numbers, the border surrounds already numbered content. Watch the example, step through each composition, then use Try it to edit the lines and wrapper order.

Swap the wrappers. Numbers now sees the frame and prefixes its top and bottom borders. The command still receives a TextBlock, but the returned lines describe a different composition.

Decorator

The order changes the output.

Caller’s render() →Source
Returned diagnostic block
check: links
broken: 2

Source: "check: links" · "broken: 2"

01/ 04
Plain diagnostic

Keep the same operation.

The caller asks render() for two diagnostic lines. No wrapper is present yet.

Reduced motion: choose a scene to see its completed state.

Read this scene

The caller asks render() for two diagnostic lines. No wrapper is present yet.

bare: 2 returned lines.

check: links
broken: 2

Order dependence is a normal part of composition: layers need not commute. If two layers depend on each other’s hidden internal state, a combined operation may be easier to reason about.

04 / Choose the boundary before choosing the wrapper

What should the frame contain?

“Add numbering and framing” leaves a decision open. State whether the frame should contain the prefixes. Then choose the stack that expresses it.

Predict which lines receive numbers.

The source has two diagnostic lines. Frame and Numbers both preserve the TextBlock interface. Which stack keeps numbering inside the border? The leftmost wrapper is outermost.

Reasoning and a stopping pointAlso available without JavaScript

Frame(Numbers(source)) formats the numbered content as one block. Numbers(Frame(source)) numbers the border as well. Both are useful compositions with different output boundaries.

Adding another formatting wrapper can change the output again. If you only need one fixed presentation, a direct formatter may be clearer than a reusable stack.

What does each layer add? Which one gets called again? Which result, error, and lifetime guarantees still belong to the inner block?

This reflection stays on this page; nothing is saved or graded.

05 / Another object, with a reference to the same collaborator

Wrapping is not copying.

Constructing Frame(source) creates a wrapper; it does not make a fresh source. Two wrappers around the same mutable block can share its state. Here each render returns a fresh array.

Rendering does not consume the plain source, so TypeScript, Go, and Python reuse one source for both comparisons. Every version shows the same independent output values. A stateful or consumable source would need a separate lifetime contract.

The target interface is deliberately small. It exposes render, not every property of the concrete source. Additional methods such as close, flush, subscribe, or seek do not become available just because the wrapper can render.

Who closes a wrapped resource?Decide ownership separately from delegation

A wrapper can own its inner resource, borrow it, or share it under a separate owner. State whether closing the wrapper closes the resource, whether other callers can still use it, and what happens to buffered work. A same-shaped render method answers none of those questions.

Here the source and wrappers hold only memory, so no close protocol is supplied.

06 / Give the assembled stack a visible owner

Setup chooses policy; the command uses the result.

Application setup chooses a text source, adds the required wrappers, and passes the resulting TextBlock to the command. Unit tests can supply the plain block directly. The caller remains unaware of the selected formatting stack.

Suppose the team wants a compact output for terminals and a framed output for reports. Make the order explicit. Silently adding another numbering wrapper can duplicate prefixes; silently moving Frame changes which lines determine its width.

ChangeDecision it requires
Support Unicode or terminal escape codesMeasure display columns and preserve control sequences deliberately; this ASCII example counts characters.
Add another formatting layerState whether it sees source lines or already formatted output. Choose the order at setup.
Use a live sourceDecide when rendering reads new state, how errors reach callers, and who owns any resources.
Cache a renderDefine freshness and invalidation. Holding an inner block does not cache its output.
Build UIs?Every component that renders lines it was handed trusts a stack it cannot see, and one day you will assemble that stack yourself.

Where it already is in your components

A panel that renders block.render() as a list of lines does not know whether those lines were numbered, framed, or both. It asks the interface and draws what comes back. That is the caller’s side of this lesson: the component depends on the interface, and whoever built the block chose the layers.

When you have to own it

Now the diagnostics appear in a browser panel as well as the terminal. Assemble the stack where the panel is set up, pass the finished block in, and display its returned lines as text in a preformatted element, with whitespace intact and horizontal scrolling for wider output. The component does not reproduce the numbering and framing rules. Text stays text, including angle brackets; it is not interpreted as HTML. The lesson’s lab works this way. A one-off component formatter can be simpler when no other consumer needs the interface.

>

07 / Find the same relationship in familiar APIs

A text block can add behavior and remain a text block.

Go’s io.TeeReader takes a Reader and returns a Reader that also writes the bytes it reads to a Writer. The write completes as part of the read, and a write failure can become a read error. Adding a side effect changes the operation’s failure contract too.

Go’s bufio.NewReader adds buffering around an io.Reader and returns a *bufio.Reader, itself an io.Reader. Its state matters even when the caller recognizes the interface: discarding a buffer or bypassing it can lose access to unread data.

Both comparisons rest on documented behavior.

What about @decorators?Related wrapping techniques, a different level of description

The ECMAScript decorators proposal, which TypeScript 5.0 implements, customizes classes and members with @name annotations. TypeScript’s older experimentalDecorators flag enables a separate, incompatible form of the idea that frameworks such as NestJS still rely on. A method decorator can install a logging wrapper, but the syntax can also serve other purposes. The object pattern here concerns a runtime object receiving and delegating to another object through a compatible interface.

Recognizing an @ annotation alone does not establish the responsibilities, order, or lifecycle of an object-decorator stack.

08 / Take the idea with you

What does this layer add, and what does it preserve?

Explain the stack without saying “Decorator”: the caller still uses this interface; this layer adds this behavior; it delegates here; this result comes back. Then swap two layers and name what would change.

Use a decorator when surrounding behavior recurs, varies independently, and belongs at a stable interface. Keep a direct call or an explicit before/after statement when there is only one place that needs it. Prefer a named combined operation when the layers cannot explain their rules independently.

Connections to follow nextRelated lessons

Adapter translates an existing interface into the one a caller needs. Decorator starts with a compatible interface and adds behavior around it. Facade presents a convenient entry point to a subsystem. Strategy supplies a chosen policy to a consumer.

Proxy concerns access to a represented object. Proxy and Decorator can have very similar code; intent and contract help explain the distinction. One operational tie-breaker: a decorator always performs the operation it wraps and adds around it, so Numbers never withholds a render. A proxy may refuse, defer, or substitute that operation. A caching wrapper may be discussed in either context. A diagram alone does not settle the name.

Composition over inheritance gives a broader view of supplying collaborators.