Structural & behavioral patterns
Learn decorator, proxy, adapter, facade, strategy, command, and middleware patterns through runnable JavaScript examples and undo redo flows.
- 01Wrap code without changing itUse a decorator or a proxy to add logging, a cache, or checks around a function or object that already works.
- 02Clean up outside dataWrite an adapter that turns outside data into your app's shape, and a facade that hides several steps behind one call.
- 03Pick, save, and chain actionsUse a strategy to pick a function by name, a command to undo and redo, and middleware to run a chain of steps.
Names for common code shapes
A design pattern is a name for a shape of code that shows up again and again. Patterns are not rules. You don't have to use them in every file. They give a team short words for common jobs, such as:
- "This function wraps another one and adds a log line."
- "This function turns another company's data into our shape."
- "This action can be undone."
Patterns come in families. This lesson covers two of them:
- Structural patterns are about how pieces of code fit together: which one wraps, guards, or talks to another. You'll meet decorator, proxy, adapter, and facade.
- Behavioral patterns are about how work gets done: which function is picked, how an action is saved, and how work passes along a chain. You'll meet strategy, command, and middleware.
On a trip, a plug adapter lets your charger fit a socket of a different shape. At the hotel, you ask the concierge, the front-desk helper, to plan dinner. The concierge calls the restaurant and the taxi for you. At an office door, a security desk checks who may enter.
- In real life: A plug adapter lets your plug fit a foreign socket
- In JavaScript: An adapter turns outside data into the shape your app uses
- In real life: A concierge takes one request and asks several staff to do the work
- In JavaScript: A facade is one simple call that makes several calls for you
- In real life: A security desk decides who may go in
- In JavaScript: A proxy decides how code can read or change an object
The table below compares the four structural patterns. Read each row as: the pattern, what it does, and one small example from this lesson.
| Pattern | What it does | Small example |
|---|---|---|
| Decorator | It wraps a function or object and adds work before or after the original call. | withRetry(fetchJson) still calls fetchJson. It only adds a second try when the first one fails. |
| Proxy | It stands in front of an object and decides what happens when code reads or changes it. | new Proxy(settings, traps) can check each new value before it is saved. |
| Adapter | It turns data or a function from outside into the shape your app uses. | fromWeatherApiB(raw) returns the same { city, celsius } shape as service A. |
| Facade | It gives one simple call that runs several steps for you, in the right order. | media.play("intro") loads the video, sets the volume, records the play, and starts it. |
Some nearby patterns have their own lessons:
- Factories and modules are in creational patterns.
- Events that many listeners can hear are in observer & pub/sub.
- Reducers and stores come next, in state management.
- How to lay out a whole project is in app architecture.
Decorator and proxy: wrap or stand in
WRAPPERSA decorator wraps a function or an object and adds a little extra work around it. The original code doesn't change. A function that wraps another function like this is a higher-order function: it takes a function and gives back a new one. Some common decorators:
withLogging(fn)prints a line each timefnis called.withRetry(fn)callsfnagain when it fails.memoize(fn)remembers earlier answers, sofndoesn't run twice for the same input.
The memoization lesson goes deeper on remembering answers. The decorators lesson covers the @decorator syntax, a newer JavaScript feature for classes. This section is about the pattern, which works with plain functions.
function withTax(fn) {
return (price) => Math.round(fn(price) * 1.1);
}
const priceWithTax = withTax((price) => price);
console.log(priceWithTax(10));Line 1 takes a function, fn. Line 2 gives back a new function. The new function calls fn, multiplies the result by 1.1 to add 10% tax, and rounds it with Math.round. The rounding is needed because 10 * 1.1 is really 11.000000000000002 in JavaScript. Line 4 wraps a function that just returns the price it gets. Line 5 prints 11.
One warning: if you wrap a method that uses this, the wrapper must pass this along. Otherwise the method loses its object.
function withLogging(fn) { return function logged(...args) { console.log("calling " + fn.name + "(" + args.join(", ") + ")"); const result = fn(...args); console.log("returned " + result); return result; };} function withMemo(fn) { const cache = new Map(); return function memoized(value) { if (cache.has(value)) return cache.get(value); const result = fn(value); cache.set(value, result); return result; };} function priceWithTax(price) { return Number((price * 1.1).toFixed(2));} const loggedPrice = withLogging(withMemo(priceWithTax));console.log(loggedPrice(20));console.log(loggedPrice(20));Read it in parts:
- Lines 1–8:
withLogginggives back a new function,logged. It prints the call, calls the original on line 4, then prints the result. - Lines 10–18:
withMemomakes a cache on line 11. A cache is a store of earlier answers; here it is aMapfrom each input to its answer. If the input is already there, line 13 returns the saved answer. If not, line 14 calls the original and line 15 saves the answer. The cache stays alive between calls becausememoizedis a closure: a function that keeps the variables around it. - Lines 20–22:
priceWithTaxis the original function. It adds 10% tax and rounds to two decimal places. - Line 24 wraps it twice. A call goes through
loggedfirst, thenmemoized, thenpriceWithTax. - Lines 25 and 26 call it twice with 20. Each call prints
calling memoized(20),returned 22, and22. The log saysmemoizedbecause that is the name of the functionwithLoggingwraps.
loggedPrice(20) passes through both wrappers to priceWithTax, which returns 22. On the second call, the cache already has 20, so priceWithTax doesn't run again.You can wrap an object the same way. The wrapper object has the same methods. Each method calls the original object's method and adds some extra work, such as timing or a permission check.
A proxy is different. It is a stand-in: an object that sits in front of another object, called the target. Code talks to the proxy, and the proxy decides what happens with each read and write. You can write a proxy as a plain object, or use JavaScript's built-in Proxy together with Reflect.
const user = { name: "Asha" };
const safeUser = new Proxy(user, {
get(target, key) { return key in target ? target[key] : "unknown"; },
});
console.log(safeUser.name, safeUser.role);Line 1 makes the target, user. Line 2 puts a Proxy in front of it. Line 3 is a trap: a function the proxy runs when code reads a property. If the target has the property, the trap returns it. If not, it returns "unknown". So line 5 prints Asha unknown, and user itself doesn't change.
Each trap is one more step between your code and the object, so errors are harder to trace. If you don't need to catch reads and writes, a plain function is simpler.
const stock = { notebook: 3 }; const guardedStock = { get(name) { return name in stock ? stock[name] : 0; }, set(name, value) { if (value < 0) throw new Error("Stock cannot be negative"); stock[name] = value; },}; guardedStock.set("pen", 2);console.log(guardedStock.get("pen"));console.log(guardedStock.get("missing")); const settings = new Proxy({ theme: "dark" }, { get(target, key, receiver) { console.log("read " + String(key)); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { if (key === "theme" && !["dark", "light"].includes(value)) { throw new Error("Unsupported theme"); } return Reflect.set(target, key, value, receiver); },}); console.log(settings.theme);settings.theme = "light";console.log(settings.theme);Read it in three parts:
- Lines 3–11 are a plain stand-in object. Its
getreturns 0 for an item that isn't in stock. Itssetthrows an error for a number below 0. Line 14 prints2, and line 15 prints0for"missing", because line 13 added 2 pens and nothing called"missing". - Lines 17–28 use the built-in
Proxy. Thegettrap printsread themeon every read (line 19). Thesettrap on line 22 throws for any theme other than"dark"or"light". Line 26 callsReflect.set, which does the normal write.Reflecthas one function for each normal object action, so a trap can do its check and then hand the action back. - Line 30 prints
read themeanddark. Line 31 changes the theme. Line 32 printsread themeandlight.
Proxies are good for checking new values, logging, loading data only when it is first read, and controlling who can change what. If a plain function call does the job, use that. It is easier to debug.
Adapter and facade: translate or simplify
BOUNDARIESAn API is the set of functions and data shapes that one piece of code offers to others. Data from outside your app often comes in a shape you don't want. It might come from a weather service, a payment company, or an old library. An adapter is a small function that turns that outside shape into your app's own shape.
The adapter sits at the boundary: the place where outside data enters your app. Then the rest of your app never sees the outside shape. If the outside shape changes, only the adapter changes.
function adaptUser(user) {
return { name: user.full_name, admin: user.role === "admin" };
}
console.log(adaptUser({ full_name: "Asha", role: "admin" }));The outside data calls the name full_name. Your app wants name. Line 2 copies the name across. It also turns the text "admin" into the true-or-false value admin: true. Line 4 prints { name: 'Asha', admin: true }. This happens once, at the boundary, not in every part of the app that shows a profile.
adaptUser() takes the outside shape, with full_name, and returns your app's shape, with name. It also turns role: "admin" into admin: true.function fromWeatherApiA(response) { return { city: response.city, celsius: response.tempC };} function fromWeatherApiB(response) { return { city: response.location.name, celsius: Math.round((response.current.tempF - 32) * 5 / 9), };} function readLegacyProfile(id, callback) { callback(null, { id, name: "Ada" });} function profilePromise(id) { return new Promise((resolve, reject) => { readLegacyProfile(id, (error, profile) => { if (error) reject(error); else resolve(profile); }); });} const reports = [ fromWeatherApiA({ city: "Chennai", tempC: 31 }), fromWeatherApiB({ location: { name: "London" }, current: { tempF: 59 } }),]; console.log(reports.map((report) => report.city + ":" + report.celsius).join(" | "));profilePromise(7).then((profile) => console.log(profile.name));Read it in parts:
- Lines 1–3 and 5–10 read two different weather shapes. Service A gives
tempC, in Celsius. Service B givestempF, in Fahrenheit, inside other objects. Both adapters return the same shape:{ city, celsius }. - Lines 25–28 run both adapters. Line 30 prints
Chennai:31 | London:15, because 59°F is 15°C. - Lines 12–14 are an old function that takes a callback: a function it calls when it is done. Lines 16–23 adapt it so it returns a promise instead: an object that will hold the result once it arrives. Line 31 prints
Ada.
Now callers can use .then() or await instead of a callback. The promisification and async/await lessons go deeper.
A facade is one simple function in front of a subsystem: a group of parts that must be called in several steps. The rest of your app calls the facade. It doesn't need to know the steps or their order.
const kitchen = {
boil() { return "boiled"; },
pour() { return "tea"; },
};
const makeTea = () => { kitchen.boil(); return kitchen.pour(); };
console.log(makeTea());Lines 1–4 are the subsystem: a kitchen with two steps. Line 5 is the facade. makeTea() boils, then pours, and returns what pour() gives back. Line 6 prints tea. The caller only knows makeTea().
A facade isn't worth it for a single step that only one place uses. It helps when several parts of the app need the same steps in the same order.
function createMediaFacade(video, analytics) { return { play(title) { video.load(title); video.setVolume(0.7); analytics.track("play:" + title); return video.start(); }, };} const video = { load(title) { console.log("load " + title); }, setVolume(level) { console.log("volume " + level); }, start() { return "playing"; },};const analytics = { track(event) { console.log("track " + event); } }; const media = createMediaFacade(video, analytics);console.log(media.play("intro"));createMediaFacade gets a video player and an analytics tool: code that counts what users do. Its play method on line 3 does four steps:
- Line 4 loads the video.
- Line 5 sets the volume.
- Line 6 records the play.
- Line 7 starts the video and returns
"playing".
Line 19 builds the facade. Line 20 is the one call the rest of the app makes. It prints load intro, volume 0.7, track play:intro, and then playing.
The steps inside can change later without changing any caller. A fetch helper is often a facade too: one call that adds login headers, reads the JSON, and turns failures into clear error messages.
Strategy: pick a function by its name
INTERACTIVEA strategy is one of several functions that do the same job in different ways. They all take the same input and give back the same kind of result. You keep them in one object, each under a name, and pick one by its name while the program runs.
This can replace a long switch statement or a long if/else chain. Shipping prices, discounts, sorting, and formatting are common examples.
const formatters = {
upper: (name) => name.toUpperCase(),
lower: (name) => name.toLowerCase(),
};
console.log(formatters.upper("Asha"));Both functions take a name and return text. Line 5 picks upper and prints ASHA. If there are only two or three choices that never change, a plain if is often clearer.
const shippingStrategies = { standard(cart) { return cart.subtotal >= 75 ? 0 : 7; }, express(cart) { return 12 + cart.items * 2; }, pickup() { return 0; },}; function quoteShipping(cart, strategyName) { return shippingStrategies[strategyName](cart);} const cart = { subtotal: 80, items: 2 };console.log(quoteShipping(cart, "standard"));console.log(quoteShipping(cart, "express"));console.log(quoteShipping(cart, "pickup"));$802$0You picked standard. Line 14 looks up shippingStrategies.standard and calls it with the cart. The fee is $0.
Try each button. Lines 1–11 hold the three shipping functions. Line 14 picks one by its name, with shippingStrategies[strategyName], and calls it with the cart. For this cart:
standardis 0, because the total, 80, is at least 75.expressis 16: 12, plus 2 for each of the 2 items.pickupis always 0.
The caller only passes a cart and a name. To add overnight shipping, you add one more function to the object. You don't edit quoteShipping. A strategy is worth it when the choices grow, come from settings, or need their own tests.
Command: actions you can undo and redo
STEP THROUGHA command is an object that stands for one action. It has two methods:
execute()does the action.undo()reverses it.
A history manager keeps commands in two stacks. A stack is a list where you add to the end and take from the end, like a pile of plates. The manager doesn't need to know what each command does. It only calls execute() and undo(). Text editors, drawing apps, task queues, and activity logs use this shape.
const todo = { done: false };
const complete = {
execute() { todo.done = true; },
undo() { todo.done = false; },
};
complete.execute();
console.log(todo.done);Line 1 is a to-do item that is not done yet. Lines 2–5 are the command. Line 6 runs it, so line 7 prints true. Calling complete.undo() would set it back to false.
Each command must know exactly how to reverse its own work. The editor below adds text at the end, so its undo removes the same number of characters from the end. That only works because nothing else changes the text in between. Real editors save a copy of the old text, or a full reverse step, for bigger edits.
A command is an object with execute() and undo(). Step through a small editor history and watch undo, redo, and the rule that a new command empties the redo list.
script
let text = initialText; return { getText: () => text, insert: (value) => { text += value; }, removeEnd: (count) => { text = text.slice(0, -count); }, };} function insertText(editor, value) { return { execute() { editor.insert(value); }, undo() { editor.removeEnd(value.length); }, label: "insert " + JSON.stringify(value), };} function createCommandManager() { const undoStack = []; const redoStack = []; return { run(command) { command.execute(); undoStack.push(command); redoStack.length = 0; }, undo() { const command = undoStack.pop(); if (!command) return; command.undo(); redoStack.push(command); }, redo() { const command = redoStack.pop(); if (!command) return; command.execute(); undoStack.push(command); }, counts: () => ({ undo: undoStack.length, redo: redoStack.length }), };} const editor = createEditor("Hi");const history = createCommandManager();history.run(insertText(editor, "!"));console.log(editor.getText(), JSON.stringify(history.counts()));history.undo();console.log(editor.getText(), JSON.stringify(history.counts()));history.redo();console.log(editor.getText(), JSON.stringify(history.counts()));history.undo();history.run(insertText(editor, "?"));console.log(editor.getText(), JSON.stringify(history.counts()));Step through the whole history:
- Line 45 runs a command that adds
!. The text isHi!. - Line 47 undoes it. The command moves from
undoStacktoredoStack. - Line 49 redoes it. The command moves back.
- Line 51 undoes it again.
- Line 52 runs a new command that adds
?. The text isHi?.
undo() moves a command from undoStack to redoStack, and redo() moves it back. After line 47, the insert "!" command waits in redoStack.The main rule is on line 25: a new command empties the redo stack. Once the user makes a new edit, the old ! belongs to a path they left, so it can't be redone.
Middleware: a chain that calls next()
IN AND OUTMiddleware is a chain of functions that each do part of the work on one request. Each function gets two things:
- a context object, shared by the whole chain, that holds the request's data;
next(), a function that runs the rest of the chain.
Code before next() runs on the way in. Code after next() runs on the way out, once the rest of the chain is done. Express and Koa, two Node.js server libraries, use this idea. So does Redux, a library for app data, for its own middleware.
const log = [];
const withLabel = (next) => { log.push("before"); next(); log.push("after"); };
withLabel(() => log.push("work"));
console.log(log.join(" > "));Line 2 is a wrapper. It pushes before, calls next(), then pushes after. Line 3 calls it and passes the main work as next. Line 4 prints before > work > after.
Real middleware also needs a plan for errors. It should also stop a function from calling next() twice, because that would run the rest of the chain twice.
function compose(middleware) { return (context) => { let index = -1; function dispatch(i) { if (i <= index) throw new Error("next() called twice"); index = i; const fn = middleware[i]; if (!fn) return; return fn(context, () => dispatch(i + 1)); } dispatch(0); return context.log; };} const stack = [ (ctx, next) => { ctx.log.push("auth before"); next(); ctx.log.push("auth after"); }, (ctx, next) => { ctx.log.push("timing before"); next(); ctx.log.push("timing after"); }, (ctx) => { ctx.log.push("handler"); },]; const app = compose(stack);console.log(app({ log: [] }).join(" -> "));auth before -> timing before -> handler -> timing after -> auth after
Each middleware calls next() to run the next one. Lines before next() run on the way in. Lines after next() run on the way out, as each call returns.
The runner is a function called compose. It keeps a counter, index, for the last middleware it ran. If a function calls next() twice, line 5 throws an error. Line 9 calls the current middleware and hands it a next that runs the one after it.
The async version below changes one thing: next() returns a promise, and each middleware waits for it with await.
Middleware is a chain of functions. Step through the async version and watch the order that await next() creates.
script
return async (context) => { let index = -1; async function dispatch(i) { if (i <= index) throw new Error("next() called twice"); index = i; const fn = middleware[i]; if (!fn) return; await fn(context, () => dispatch(i + 1)); } await dispatch(0); return context.log; };} const stack = [ async (ctx, next) => { ctx.log.push("auth before"); await next(); ctx.log.push("auth after"); }, async (ctx, next) => { ctx.log.push("timing before"); await next(); ctx.log.push("timing after"); }, async (ctx) => { ctx.log.push("handler"); },]; async function main() { const app = composeAsync(stack); const log = await app({ log: [] }); console.log(log.join(" -> "));}main();Because of await next(), the order is like the layers of an onion. Auth starts on the outside. Timing starts inside it. The handler is the middle. Then timing finishes, and auth finishes last.
await next() finishes, so the after lines run on the way out, in reverse order.Choosing a pattern without extra code
Patterns often mix. A decorator may use a closure. A proxy may keep a cache. A facade may call adapters. That is normal. Pick the name that best explains the job of the code you are writing.
The table below says when each pattern is worth it. Read each row as: the pattern, how often it pays off, and the job that calls for it.
| Pattern | Is it worth it? | Use it when |
|---|---|---|
| Decorator | Often. | Many functions need the same extra work, such as logging, a second try, a cache, timing, or a permission check. |
| Proxy | Sometimes. It is powerful but harder to debug. | Every read or write of an object must be checked, logged, or delayed, and callers should not touch the object directly. |
| Adapter or facade | Very often. | Your code talks to outside services, libraries, browser features, or old functions that take callbacks. |
| Strategy | When the choices keep changing. | You pick one of several ways to do a job while the program runs, such as shipping prices, discounts, sorting, or formatting. |
| Command | When you need a history. | Actions must be undone, redone, saved in a queue, tried again, or written to a log. |
| Middleware | When work passes through steps. | A request or action passes through login checks, logging, and timing before the main handler runs. |
const examples = [ "withRetry(fetchJson)", "new Proxy(settings, traps)", "fromWeatherApiB(response)", "shippingStrategies.express(cart)", "history.run(insertText(editor, '!'))", "await logger(ctx, next)",]; console.log(examples.length);Each text in this list is a call you met in this lesson. Line 10 prints 6, the number of calls. Now sort the same calls by the job they do.
const reliableFetch = withRetry(fetchJson);new Proxy(settings, { set(target, key, value) { ... } })fromWeatherApiB(rawResponse)media.play("intro")shippingStrategies[selected](cart)history.run(insertText(editor, "!"))
Sort each card by the job its code does.
Three more patterns are worth knowing by name:
- Composite treats one item and a group of items the same way, like a file and a folder.
- Iterator is a standard way to walk through a collection, one item at a time.
- Chain of responsibility passes a request along a line of handlers until one of them handles it. Middleware is a kind of chain where every step shares one context and calls
next().
Common mix-ups
- "Decorator means the
@decoratorsyntax." The syntax is one language feature. The pattern is just wrapping, and plain functions can do it. - "Proxy always means the built-in
Proxy." A plain stand-in object is also a proxy when it guards how code reads or changes another object. - "Adapter and facade mean the same thing." An adapter changes a shape. A facade hides several steps behind one call.
- "A strategy is always better than
switch." A shortswitchis often clearer, until the choices grow or come from settings. - "Middleware is only for servers." Redux, logging in the browser, feature switches, and analytics can all use the same chain idea.
The table below pairs up patterns that look alike. Read each row as: the two names, one question that tells them apart, and which one to pick.
| Mix-up | Question to ask | Which one to pick |
|---|---|---|
| Decorator or proxy | Am I adding work around a call, or guarding how code reads and changes an object? | Pick a decorator to add work. Pick a proxy to guard an object. |
| Adapter or facade | Am I changing a shape, or hiding several steps behind one call? | Pick an adapter to change a shape. Pick a facade to hide steps. |
| Strategy or command | Am I picking a function to run now, or saving an action to undo later? | Pick a strategy to choose. Pick a command to keep a history. |
| Middleware or chain of responsibility | Does every step share one context and call next(), or does one handler take the request and stop? | Pick middleware when every step runs. Pick a chain when one handler takes over. |
Practice exercises
What does the last line print?
function withLogging(fn) {
return (...args) => {
const result = fn(...args);
console.log("result:" + result);
return result;
};
}
const shout = withLogging((word) => word.toUpperCase());
console.log(shout("pattern"));function withLogging(fn) {
return (...args) => {
const result = fn(...args);
console.log("result:" + result);
return result;
};
}
const shout = withLogging((word) => word.toUpperCase());
console.log(shout("pattern"));The wrapper prints result:PATTERN, then returns PATTERN. So the last line prints PATTERN.
Follow the adapter and type what the last line prints.
function adaptPaymentA(response) {
return { id: response.payment_id, cents: response.amount_cents, ok: response.status === "paid" };
}
const payment = adaptPaymentA({ payment_id: "p1", amount_cents: 1299, status: "paid" });
console.log(payment.id + ":" + payment.cents + ":" + payment.ok);function adaptPaymentA(response) {
return { id: response.payment_id, cents: response.amount_cents, ok: response.status === "paid" };
}
const payment = adaptPaymentA({ payment_id: "p1", amount_cents: 1299, status: "paid" });
console.log(payment.id + ":" + payment.cents + ":" + payment.ok);The adapter returns { id: "p1", cents: 1299, ok: true }. Line 5 joins them with colons, so it prints p1:1299:true.
What number does the code print?
const discounts = {
student: (total) => total * 0.8,
none: (total) => total,
};
console.log(discounts.student(50));const discounts = {
student: (total) => total * 0.8,
none: (total) => total,
};
console.log(discounts.student(50));discounts.student(50) returns 50 * 0.8, which is 40.
Follow the text and the redo list, then type what the last line prints.
function createEditor(text = "") {
return {
text,
insert(value) { this.text += value; },
removeEnd(count) { this.text = this.text.slice(0, -count); },
};
}
function insertText(editor, value) {
return {
execute() { editor.insert(value); },
undo() { editor.removeEnd(value.length); },
};
}
const undo = [];
const redo = [];
const editor = createEditor("A");
const command = insertText(editor, "B");
command.execute();
undo.push(command);
undo.pop().undo();
redo.push(command);
insertText(editor, "?").execute();
redo.length = 0;
console.log(editor.text + " redo=" + redo.length);function createEditor(text = "") {
return {
text,
insert(value) { this.text += value; },
removeEnd(count) { this.text = this.text.slice(0, -count); },
};
}
function insertText(editor, value) {
return {
execute() { editor.insert(value); },
undo() { editor.removeEnd(value.length); },
};
}
const undo = [];
const redo = [];
const editor = createEditor("A");
const command = insertText(editor, "B");
command.execute();
undo.push(command);
undo.pop().undo();
redo.push(command);
insertText(editor, "?").execute();
redo.length = 0;
console.log(editor.text + " redo=" + redo.length);The text goes back to A, then gets ?, and redo is emptied. The last line prints A? redo=0.
Type the log in the exact order it is printed.
const log = [];
const outer = (next) => { log.push("outer before"); next(); log.push("outer after"); };
const inner = (next) => { log.push("inner before"); next(); log.push("inner after"); };
outer(() => inner(() => log.push("handler")));
console.log(log.join(" > "));const log = [];
const outer = (next) => { log.push("outer before"); next(); log.push("outer after"); };
const inner = (next) => { log.push("inner before"); next(); log.push("inner after"); };
outer(() => inner(() => log.push("handler")));
console.log(log.join(" > "));The before lines run on the way in, then the handler, then the after lines on the way out: outer before > inner before > handler > inner after > outer after.
Check your understanding
Question 1 of 7Which sentence best describes the decorator pattern in JavaScript?
Choose an answer to see the explanation.
Question 2 of 7What does the express shipping strategy print?
Read the code, then predictconst shippingStrategies = { standard(cart) { return cart.subtotal >= 75 ? 0 : 7; }, express(cart) { return 12 + cart.items * 2; }, pickup() { return 0; }, }; const cart = { subtotal: 80, items: 2 }; console.log(shippingStrategies.express(cart));Choose an answer to see the explanation.
Question 3 of 7Which pair is most accurate?
Choose an answer to see the explanation.
Question 4 of 7What is the final command-manager state?
Read the code, then predictconst editor = { text: "Hi" }; const undo = []; const redo = []; function insert(value) { return { execute() { editor.text += value; }, undo() { editor.text = editor.text.slice(0, -value.length); }, }; } let command = insert("!"); command.execute(); undo.push(command); undo.pop().undo(); redo.push(command); command = insert("?"); command.execute(); undo.push(command); redo.length = 0; console.log(editor.text + " redo=" + redo.length);Choose an answer to see the explanation.
Question 5 of 7What does the middleware onion print?
Read the code, then predictconst log = []; const outer = (next) => { log.push("outer before"); next(); log.push("outer after"); }; const inner = (next) => { log.push("inner before"); next(); log.push("inner after"); }; outer(() => inner(() => log.push("handler"))); console.log(log.join(" > "));Choose an answer to see the explanation.
Question 6 of 7When is a proxy a better fit than a decorator?
Choose an answer to see the explanation.
Question 7 of 7Which advice keeps these patterns helpful in real projects?
Choose an answer to see the explanation.
Key takeaways
- A decorator wraps a function or object and adds work around it. A proxy stands in front of an object and guards it.
- An adapter changes outside data into your shape. A facade hides several steps behind one call.
- A strategy is one of several functions, picked by name while the program runs.
- A command is an action object with
execute()andundo(), so a history can undo and redo it. - Middleware is a chain of functions joined by
next(). Withawait next(), the after lines run on the way out.
Use a pattern when the code really does its job: wrapping, guarding, translating, picking, undoing, or chaining.
Next, state management shows how to keep your app's data in one place and change it step by step, with actions and reducers, as the app grows.