← Back to PatternPip

DECORATOR / THE WALKTHROUGH

Keep the sender.
Wrap the extras.

From one local email sender to independent, composable responsibilities.

CHAPTER 1 OF 3

Send a message. Return a receipt.

The client calls send(message). Our local EmailSender accepts the message and returns a receipt; no real email leaves this page.

Send a sample message and follow the call and its return.

ONE CALL, OPTIONAL EXTRAS

One message. One sender.

Try it yourself

This is an offline notification simulator. Tracking records local acceptance, not delivery or a read confirmation.

Optional wrappers

USES SENDER

Client

send(message)

Same call for every combination.

CORE BEHAVIOR

EmailSender

send(message)

Accept the message and return a receipt.

No receipt yetThe result appears after the call returns to the client.
Recorded call sequence · 0 events

Send a sample to see the actual order of calls and side effects.

Choose your extras, then send a local sample message.

Try this: send a sample, then simulate a failure. A failed call returns no receipt.

Reduced motion is on; the whole trace appears immediately. Each run starts a fresh local sender. No real messages are sent.

THE SIMPLE START

The client asks. The sender handles email.

The client depends on the small Sender interface. Our EmailSender implements it. The receipt means this local simulator accepted the message; it says nothing about a real inbox.

The shared interface · TypeScript

Every implementation accepts a message and returns a receipt, or throws an error.

export type Receipt = Readonly<{ id: string; status: "accepted" }>;
export interface Sender {
  send(message: string): Receipt;
}
The client’s one call · TypeScript

The client uses send() without needing to know the concrete sender.

export function notify(sender: Sender, message: string): Receipt {
  return sender.send(message);
}
The core sender · TypeScript

This offline implementation validates the message and creates an immutable receipt. No network service is involved.

// Offline stand-in: acceptance is not a delivery or read confirmation.
export class EmailSender implements Sender {
  private record: RecordEvent;
  private fail: boolean;
  private sequence = 0;
  constructor(record: RecordEvent, fail = false) {
    this.record = record;
    this.fail = fail;
  }
  send(message: string): Receipt {
    this.record({
      actor: "email",
      phase: "call",
      text: "Email sender receives the message.",
    });
    if (!message.trim() || message.length > 160)
      throw new Error("Use a message of 1–160 characters.");
    if (this.fail) {
      this.record({
        actor: "email",
        phase: "error",
        text: "Email sender fails. No receipt is created.",
      });
      throw new Error("Local email service unavailable.");
    }
    const receipt = Object.freeze({
      id: `local-${++this.sequence}`,
      status: "accepted" as const,
    });
    this.record({
      actor: "email",
      phase: "return",
      text: `Email sender accepts the message: ${receipt.id}.`,
    });
    return receipt;
  }
}

Chapter changes start fresh. Theme and language preserve the current example.