Creational patterns
Learn factory functions, singletons, builders, modules, revealing modules, and cloning choices for production JavaScript architecture.
- 01Pick the right way to make objectsSay when a factory, a singleton, a builder, a module, or a copy makes your code clearer.
- 02Follow real code step by stepTrace a factory that keeps private data, a singleton made on first use, and a builder that saves choices in steps.
- 03Avoid common pattern mistakesSpot shared data that tests can't swap, copies that share inner objects, lost
this, and patterns used where a plain object would do.
Patterns are names for common designs
Programmers solve the same kinds of problems again and again. Over time, the common solutions got names. A design pattern is one of those names. It lets a team say "use a factory here" in three words, instead of explaining the whole design each time. A pattern is a shared word, not a rule you must follow.
The best-known list of patterns comes from a 1994 book by four authors, often called the "Gang of Four" book. It used the languages C++ and Smalltalk. JavaScript has tools those languages lacked, so some of those patterns become very small here:
- A shared object can be one object exported from a module file.
- Private data can live in a closure: variables a function keeps after it returns.
- A long chain of setup steps can often be one object of options with default values.
Creational patterns are names for ways to make objects. They answer four questions: how do I create an object, set it up, share it, and copy it? In this lesson you write each one as real JavaScript: factory functions, singletons, builders, modules, and copies.
Some of these ways look alike at first. The table below puts them side by side. Read each row as: "this way makes objects like this, it suits these jobs, and here is what to watch for."
| Way to make objects | How it makes them | When it helps | What to watch for |
|---|---|---|---|
| Factory function | A plain function makes an object and returns it. | When objects differ a little, need private data, or when you want to avoid new and this mistakes. | Each call makes a new object and new copies of its methods. |
| Class | new runs the constructor. The methods are shared on the prototype. | When many objects share methods, or you need instanceof or a parent class. | Forgetting new, losing this in a saved method, or too many layers of parent classes. |
| Constructor function | The older way to write a class: new Fn() sets values on this, and methods go on Fn.prototype. | When you read older code, or want to see what a class does inside. | Without new, it runs as a normal function, and this is the wrong object. |
| Singleton | Only one object is made, and every caller gets that same object. | Settings, one shared logger, or feature flags. | Tests can't easily swap the shared object, so one test can change what the next test sees. |
| Builder | Methods save choices one at a time. Then build() checks them and returns the result. | Queries, test data, and big objects made in steps. | Too many steps when one options object with defaults would do. |
This lesson uses ideas from object basics, constructors and new, class basics, closures, closure patterns, Object.create, private members, why modules exist, import and export, and immutability.
Factory functions
CHANGE INPUTSA factory function is a plain function that makes an object and returns it. You call it like any other function, with no new in front. It helps in three ways:
- It can keep private data in a closure, where outside code can't reach it.
- It can return a different kind of object depending on what you pass in.
- It avoids two common class mistakes: forgetting to write
new, and losingthis, the object a method works on.
You ask a cook for tea and say how you like it. The cook makes a fresh cup and hands it to you. The next person gets their own cup, made the way they asked.
- In real life: You ask for tea
- In JavaScript: You call the factory with an input
- In real life: The cook hands you a fresh cup
- In JavaScript: The factory returns a new object
- In real life: You choose with or without sugar
- In JavaScript: The input changes what you get
function createUser(name) {
return { name, greet: () => "Hi, " + name };
}
const user = createUser("Asha");
console.log(user.greet());Line 1 starts the factory, createUser. Line 2 returns a new object. Its greet function remembers name, because a function keeps the variables around it. Line 4 asks for Asha's object, and line 5 prints Hi, Asha.
function createCounter(label) { let count = 0; return { label, next() { count += 1; return label + ": " + count; }, reset() { count = 0; }, };} const first = createCounter("factory");const second = createCounter("factory");console.log(first.next());console.log(first.next());console.log(second.next());console.log(first === second);Read it line by line:
- Line 1 starts an ordinary function,
createCounter. - Line 2 makes a new
countvariable, just for this one call. - Lines 5 to 8 are the
nextmethod on the returned object. It can still reachcount, but nothing outside can. - Lines 15 and 16 call the factory twice. That makes two objects, each with its own
count. - Lines 17 to 19 print
factory: 1,factory: 2, andfactory: 1. The second counter starts from zero. - Line 20 prints
false: the two objects are different, even though both have the same label.
createCounter() makes a new object with its own private count. So first and second are two different objects.function createUser(name) { return { name, greet() { return "Hello, " + name; }, };} class ClassUser { constructor(name) { this.name = name; } greet() { return "Hello, " + this.name; }} function ConstructorUser(name) { this.name = name;}ConstructorUser.prototype.greet = function () { return "Hello, " + this.name;}; console.log(createUser("Ada").greet());console.log(new ClassUser("Lin").greet());console.log(new ConstructorUser("Grace").greet());All three make an object with a greet method. They differ in how:
- The factory (line 1) returns an object, and
greetreadsnamefrom the closure. - The class (line 10) is called with
newand savesnameonthis. - The constructor function (line 19) is the older way to write a class.
new ConstructorUser()makes the object, andgreetis added to its prototype: a shared object that all its objects look in for methods.
Lines 26 to 28 print Hello, Ada, Hello, Lin, and Hello, Grace.
There is one cost to a factory. Each call makes a new greet function. A class keeps one greet on its prototype, shared by every object. That matters only when you make very many objects.
If you use TypeScript, its types are removed before the code runs. So when a factory gets data from a form or from JSON, check that data in code too.
function createNotifier(channel) { if (channel === "email") { return { send(message) { return "email: " + message; }, }; } if (channel === "sms") { return { send(message) { return "sms: " + message; }, }; } return { send(message) { return "toast: " + message; }, };} console.log(createNotifier("email").send("Build passed"));console.log(createNotifier("sms").send("Deploy now"));createNotifier("email")email: Build passedcreateNotifier("email") returns an object with the same send method as the others. This one prints email: Build passed.
The notifier above is a factory that picks one of three objects: email, SMS, or toast. All three have the same send method, so the code that calls it doesn't change. A bigger version of this idea is called an abstract factory. It makes a whole set of objects that belong together. For example, one UI factory could return a matching createButton and createDialog. Later lessons cover that. For now, remember: one function can hide the choice and return objects with the same methods.
Singletons
STEP THROUGHA singleton is an object that exists only once. Every part of the program that asks for it gets that same object.
In JavaScript, a module file is the easiest way to make one. A file with import and export runs only once, even if many files import it. So every importer gets the same exported object. This sharing has limits. Each worker, each browser tab, and each Node process runs its own copy of the module.
globalThis.moduleLoads = (globalThis.moduleLoads ?? 0) + 1;
export const cart = { items: [] };Line 1 counts how many times this file has run. Line 2 exports one cart object.
import { cart as first } from "./cart.mjs";
import { cart as second } from "./cart.mjs";
first.items.push("tea");
console.log(first === second);
console.log(second.items.join(", "));
console.log(globalThis.moduleLoads);In consumer.mjs, lines 1 and 2 import the cart from the same file, under two names. Line 3 adds tea through first. Lines 4 to 6 print true, tea, and 1:
true: both names point at the same object.tea: the item added throughfirstshows up throughsecond.1:cart.mjsran only once.
These are two real module files, so they can't run in the page's editor. They have no Run button.
A singleton can also be lazy: it isn't made until the first time someone asks for it. Step through the example below and watch the second call.
A lazy singleton makes its one object on the first call. Every later call gets that same object back. Step through both calls and watch the second argument get ignored.
script
function getConfig(env) { if (!configInstance) { configInstance = Object.freeze({ env, apiBase: "https://" + env + ".example.com", retries: 2, }); } return configInstance;} const first = getConfig("prod");const second = getConfig("staging");console.log(first === second);console.log(second.env);console.log(Object.isFrozen(first));"staging" is ignored.function createCounter(label) { let count = 0; return { next() { count += 1; return label + ": " + count; }, };} let configInstance;function getConfig(env) { if (!configInstance) configInstance = { env }; return configInstance;} const firstCounter = createCounter("box");const secondCounter = createCounter("box");const firstConfig = getConfig("prod");const secondConfig = getConfig("staging");console.log(firstCounter === secondCounter);console.log(firstCounter.next());console.log(secondCounter.next());console.log(firstConfig === secondConfig);console.log(secondConfig.env);falsebox: 1box: 1Two factory calls make two different objects. Each first next() call prints 1, because each object has its own count.
Singletons are fine for things that never change while the program runs, like settings, one shared logger, or feature flags. They cause trouble in tests. A test can't easily give the code a different object, because the code reaches for the singleton itself. The fix is to pass the logger or settings into the function as an argument. That is called dependency injection. The mocks and fakes lesson uses the same idea.
function welcomeUser(user, send) {
return send("Welcome, " + user.name);
}
const messages = [];
welcomeUser({ name: "Asha" }, (message) => messages.push(message));
console.log(messages[0]);Line 1 takes send as an argument. Line 5 passes a small fake send. It saves the message in an array instead of sending an email. Line 6 prints Welcome, Asha. You don't need any library for this. Real code passes a real sender; a test passes a fake.
Object.freeze stops anyone from changing an object's own properties. That suits a settings object. But it doesn't make code easier to test. It is also shallow: objects inside the frozen object can still change, unless you freeze them too. Use a singleton only when sharing one object is the goal.
Builders
STEP THROUGHA builder helps when you make something in several steps. It works like this:
- Each method saves one choice.
- Each method returns the builder (
this), so you can call the next method right after it, like.select().where(). This is called chaining. - A last method,
build(), checks the saved choices and returns the finished result.
const request = { method: "GET", path: "/users" };
console.log(request.method + " " + request.path);Line 1 makes the whole request in one go. Line 2 prints GET /users. When an object is this small, a plain object is easier to read than a chain of builder methods.
A builder saves choices over several method calls. Then build() checks them and returns the finished result.
script
const state = { table, fields: ["*"], filters: [], limit: null, }; return { select(...fields) { state.fields = fields; return this; }, where(field, value) { state.filters.push({ field, value }); return this; }, limit(count) { state.limit = count; return this; }, build() { if (!state.table) throw new Error("table is required"); if (state.limit !== null && state.limit <= 0) { throw new Error("limit must be positive"); } const fields = state.fields.join(", "); const where = state.filters.length ? " WHERE " + state.filters.map((filter) => filter.field + " = " + JSON.stringify(filter.value) ).join(" AND ") : ""; const limit = state.limit === null ? "" : " LIMIT " + state.limit; return "SELECT " + fields + " FROM " + state.table + where + limit; }, };} const query = createQueryBuilder("users") .select("id", "email") .where("active", true) .limit(2) .build();console.log(query);select(), where(), and limit() each save one choice and return the same builder. Only build() checks the choices and makes the final string.This query builder is small, but it has every part of a real one. select, where, and limit save choices. build() throws an error if the table is missing or the limit is zero or less. Otherwise it returns a string. A real query builder would also protect against unsafe input. It would not glue SQL together from strings by hand.
function createApiClient({ baseUrl = "/api", timeout = 3000, headers = {},} = {}) { return { baseUrl, timeout, headers };} const fastClient = createApiClient({ timeout: 1000 });console.log(fastClient.baseUrl);console.log(fastClient.timeout);Lines 1 to 5 take one options object and give each option a default value. Line 9 passes only timeout. Line 10 prints the default /api, and line 11 prints 1000.
Many JavaScript functions should stop here. An options object is short, and it works well with parameters and destructuring. Use a builder only when there are real steps to check.
A builder also keeps its choices between calls. Start a new builder for each query. If you reuse one, each where() call adds to the filters already saved there.
Module and revealing module
HISTORICALBefore module files with import and export worked everywhere, people used an IIFE to keep data private. IIFE stands for "immediately invoked function expression": a function that runs as soon as it is written. Its variables stay private. It returns an object with only the functions other code may use.
const cartModule = (() => { const items = []; function add(name, price) { items.push({ name, price }); } function total() { return items.reduce((sum, item) => sum + item.price, 0); } return { add, total };})(); cartModule.add("Book", 12);cartModule.add("Pen", 3);console.log(cartModule.total());console.log(cartModule.items);Read it line by line:
- Line 1 starts a function and line 10 runs it straight away, with
(). - Line 2 makes the
itemsarray. It is private, because it is never returned. - Lines 3 to 8 write two private functions,
addandtotal. - Line 9 returns an object with just those two functions.
- Lines 12 and 13 add a book and a pen. Line 14 prints
15. - Line 15 prints
undefined, becauseitemsis not on the returned object.
This is called a revealing module: you write private functions first, then return, or "reveal", the ones you choose.
let currentUser = null; export function setCurrentUser(user) { currentUser = user;} export function getCurrentUser() { return currentUser;}In a module file, every top-level variable is private unless you export it. Here currentUser (line 1) is private. Only setCurrentUser (line 3) and getCurrentUser (line 7) are exported. And because a module file runs only once, an object made at its top level is JavaScript's usual singleton. Review why modules exist and import and export if this is new to you.
Cloning and copying
REAL COPIESThe prototype pattern makes new objects by copying an existing one. Before you copy, ask what the copy should keep:
- the object's own properties,
- objects stored inside it,
- its methods from a class,
- its private fields,
- its prototype, the shared object it looks in for methods.
const widgetPrototype = { describe() { return this.kind + ":" + this.theme; }, clone(overrides = {}) { return Object.assign( Object.create(Object.getPrototypeOf(this)), this, overrides ); },}; const primary = Object.assign(Object.create(widgetPrototype), { kind: "button", theme: "primary",});const danger = primary.clone({ theme: "danger" });console.log(primary.describe());console.log(danger.describe());console.log(Object.getPrototypeOf(danger) === widgetPrototype);widgetPrototype holds two shared methods, describe and clone. Inside clone():
Object.create(Object.getPrototypeOf(this))(line 7) makes a new empty object with the same prototype as the original.Object.assign(line 6) copies the original's own properties onto it, then the changes inoverrides.
Line 18 makes danger, a copy of primary with a new theme. Lines 19 to 21 print button:primary, button:danger, and true: the copy kept the same prototype.
class Point { constructor(xOrPoint, y) { if (typeof xOrPoint === "object") { this.x = xOrPoint.x; this.y = xOrPoint.y; } else { this.x = xOrPoint; this.y = y; } } clone() { return new Point(this.x, this.y); } toString() { return this.x + "," + this.y; }} const original = new Point(2, 3);const cloned = original.clone();const copied = new Point(original);console.log(cloned.toString());console.log(copied.toString());console.log(copied instanceof Point);There are two ways to copy here:
- Line 20 calls
clone()on the old point. The method (line 11) makes the copy. - Line 21 calls
new Point(original). This is a copy constructor: the constructor (line 2) sees an object and copies itsxandy.
Both print 2,3, and line 24 prints true: the copy is a real Point. Either way, the class decides how deep the copy goes.
const original = { user: { name: "Ada" }, tags: ["js"],};const spread = { ...original };spread.user.name = "Lin";spread.tags = [...spread.tags, "patterns"];console.log(original.user.name);console.log(original.tags.join(","));console.log(spread.tags.join(","));The spread syntax { ...original } (line 5) makes a shallow copy: a new outer object, but the objects inside it are shared, not copied.
- Line 6 changes
spread.user.name. Both objects share oneuser, so line 8 printsLinfor the original too. - Line 7 puts a brand new array in
spread.tags. The original array doesn't change, so line 9 printsjsand line 10 printsjs,patterns.
{ ...original } makes a new outer object, but both user properties point at the same inner object. So changing spread.user.name also changes original.user.name.class Point { constructor(x, y) { this.x = x; this.y = y; } distance() { return Math.hypot(this.x, this.y); }} const point = new Point(3, 4);const copied = structuredClone(point);console.log(copied instanceof Point);console.log(typeof copied.distance);console.log(JSON.stringify(copied));structuredClone makes a deep copy: it copies the inner objects too. It works for plain data, like arrays, plain objects, dates, Maps, and Sets. But it doesn't keep a class's prototype. So line 12 copies x and y, but:
- Line 13 prints
false: the copy is not aPoint. - Line 14 prints
undefined: thedistancemethod is gone. - Line 15 prints
{"x":3,"y":4}: the data is all there.
The table below compares the ways to copy. Read each row as: "this way of copying goes this deep, and it is best for this."
| Way to copy | How deep does it copy? | When to use it |
|---|---|---|
Spread, { ...obj } | Only the top level. Inner objects are shared. | Plain objects and arrays, when sharing the inner objects is fine. |
Object.create(proto) | It makes a new object that keeps the same prototype. | Objects made by the prototype pattern, with a clone() method and shared methods. |
A clone() method | The object's own method decides. | When the object should set its own copy rules, including private fields. |
| A copy constructor | The class decides. | new Point(other) says clearly that it copies, and the result is a real Point. |
structuredClone | All the way down, for plain data. | Dates, Maps, Sets, arrays, and plain objects. A class object comes back as a plain object. |
Where these appear in real projects
SORT ITReal code rarely says "this is a pattern". It uses names like createApiClient, logger, queryBuilder, aUser(), or clone(). Knowing the pattern helps you talk about the design and its costs.
The table below shows common jobs. Read each row as: "for this job, people often use this way, for this reason."
| The job | The usual way | Why it fits |
|---|---|---|
| Talking to a server (an API client) | A factory or an options object | createApiClient({ baseUrl, token }) keeps the server address and key in one place, not in every call. |
| App settings | A singleton exported from a module | A frozen settings object is fine when it is made once at startup and never changes. |
| Database queries | A builder | Filters added one step at a time are easy to read, and build() can check them before the query is sent. |
| Test data | A builder or a factory | aUser().withRole("admin").build() lets a test say only what matters, and still returns a plain object. |
| Sets of UI parts | An abstract factory | One factory returns a set of objects that belong together, such as a matching dialog and button. |
When a pattern is too much
Pick the smallest thing that works:
- If one place makes one fixed object, write the object directly, with
{ }. - If the object changes a little with each call, use a factory.
- If a module already holds one shared service, export it.
Use a named pattern only when it makes the choices, the saved data, the checks, or the copying easier to see.
Object pools: reuse only when it helps
An object pool keeps used objects and hands them out again, instead of making a new one each time. It can help for things that are slow to make or limited in number, like database connections, when the database library doesn't pool them already. For ordinary JavaScript objects, just make new ones.
function createPool(make, reset) {
const available = [];
return {
acquire() { return available.pop() ?? make(); },
release(item) { reset(item); available.push(item); },
};
}
const pool = createPool(() => ({ value: "" }), (item) => { item.value = ""; });
const first = pool.acquire();
first.value = "old";
pool.release(first);
const reused = pool.acquire();
console.log(first === reused);
console.log(reused.value === "" ? "reset" : reused.value);Read it line by line:
- Lines 1 to 7 make the pool. It keeps returned objects in the
availablearray. - Line 4,
acquire(), hands out a kept object, or callsmake()if there is none. - Line 5,
release(), clears the object withresetand keeps it. - Lines 9 to 12 take an object, write
oldinto it, give it back, and take one again. - Lines 13 and 14 print
trueandreset: the same object came back, with its old value cleared.
This small pool has gaps. Nothing stops code from giving the same object back twice, and the pool can grow without limit. A real pool needs rules for both. A pool also keeps memory in use, and it can leak old data if the reset misses a property. So measure first, and add a pool only if it clearly helps.
createNotifier(channel)returns an email, SMS, or toast sender.getConfig()returns the same frozen object after the first call.`query.select("id").where("active", true).build()``const p2 = new Point(p1);``createCounter("cart")` returns an object with a private `count`.`userBuilder().withRole("admin").build()``const copy = structuredClone(settings);``export const logger = createLogger();`
Sort each card by the problem it solves. Does it make new objects, share one object, build in steps, or copy?
Common misconceptions
- "You must use patterns." Patterns are only names. Use the smallest JavaScript code that solves the problem.
- "A factory is just a class without the word
class." A factory can pick which object to return, and it can keep data private in a closure. A class shares its methods through a prototype. - "A frozen singleton is safe." Freezing stops changes to its top-level properties. But it is still one shared object, and tests still can't swap it out.
- "Every chain of methods is a builder." A builder saves choices and has a
build()that checks them. Some chains are just handy helper methods. - "
structuredClonecopies everything." It copies plain data deeply, but a class object comes back without its prototype or its methods.
The table below gives a quick check for each mix-up. Read each row as: "if I believe this, I ask this question, and the better way to say it is this."
| The mix-up | Ask yourself | A better way to say it |
|---|---|---|
| A factory and a class differ only in how they look. | Does the code pick which object to return, or need private data in a closure? | Factories and classes suit different jobs. |
| A singleton is the same as a global variable. | Is the sharing on purpose, read-only, and easy to swap in tests? | A singleton is one shared object that you control, but it is still shared. |
| A builder always makes code cleaner. | Would one options object with defaults say the same thing? | Use a builder when there are real steps and checks. |
| A deep copy keeps class methods. | Does the copy still pass instanceof, and does it still have its methods? | Copying the data and copying a class object are different things. |
Practice exercises
5 EXERCISESType the three printed values in order.
function makeBox(label) {
let value = 0;
return {
add() {
value += 1;
return label + ":" + value;
},
};
}
const first = makeBox("cart");
const second = makeBox("cart");
first.add();
console.log(first.add());
console.log(second.add());
console.log(first === second);The first box reaches cart:2. The second box has its own value, so it prints cart:1. They are two different objects, so the last line prints false.
Read which object the factory picks, then type the exact text printed.
function createNotifier(channel) {
return channel === "sms"
? { send: (message) => "sms: " + message }
: { send: (message) => "email: " + message };
}
console.log(createNotifier("sms").send("Ready"));The factory picks the SMS object, so send("Ready") prints sms: Ready.
Type the URL the builder prints.
function createUrlBuilder(path) {
const params = [];
return {
param(name, value) {
params.push(name + "=" + encodeURIComponent(value));
return this;
},
build() {
return path + "?" + params.join("&");
},
};
}
console.log(createUrlBuilder("/users").param("role", "admin user").build());The builder saves one param with the space written as %20. Then build() returns /users?role=admin%20user.
Type the three printed values in order.
const original = { nested: { count: 1 }, tags: ["a"] };
const copy = { ...original };
copy.nested.count += 1;
copy.tags = [...copy.tags, "b"];
console.log(original.nested.count);
console.log(original.tags.join(""));
console.log(copy.tags.join(""));nested is shared, so the original sees the count go to 2. The original's tags stay a. The copy's new tags array joins to ab.
Type the two values the code prints.
class Widget {
constructor(name) {
this.name = name;
}
label() {
return "widget:" + this.name;
}
}
const copied = structuredClone(new Widget("menu"));
console.log(copied instanceof Widget);
console.log(typeof copied.label);The copy is plain data, not a Widget. So the output is false, then undefined.
Check your understanding
8 QUESTIONSQuestion 1 of 8What is a design pattern, in this lesson?
Choose an answer to see the explanation.
Question 2 of 8What does the factory counter print?
Read the code, then predictfunction makeCounter() { let count = 0; return { next: () => ++count }; } const a = makeCounter(); const b = makeCounter(); console.log(a.next()); console.log(b.next()); console.log(a === b);Choose an answer to see the explanation.
Question 3 of 8What does this singleton code print?
Read the code, then predictlet instance; function getLogger(name) { if (!instance) instance = { name }; return instance; } const first = getLogger("app"); const second = getLogger("test"); console.log(first === second); console.log(second.name);Choose an answer to see the explanation.
Question 4 of 8What does this builder print?
Read the code, then predictfunction makeBuilder() { const parts = []; return { add(value) { parts.push(value); return this; }, build() { return parts.join("/"); }, }; } console.log(makeBuilder().add("api").add("users").build());Choose an answer to see the explanation.
Question 5 of 8What does this revealing module print?
Read the code, then predictconst module = (() => { const secret = "inside"; function reveal() { return secret; } return { reveal }; })(); console.log(module.reveal()); console.log(module.secret);Choose an answer to see the explanation.
Question 6 of 8What does this print when
structuredClonecopies a class object?Read the code, then predictclass Point { constructor(x) { this.x = x; } double() { return this.x * 2; } } const copy = structuredClone(new Point(4)); console.log(copy instanceof Point); console.log(typeof copy.double);Choose an answer to see the explanation.
Question 7 of 8What is usually the simpler choice instead of a builder?
Choose an answer to see the explanation.
Question 8 of 8When should you think about an object pool?
Choose an answer to see the explanation.
Key takeaways
- Patterns are names for common designs, not rules you must follow.
- A factory returns a new object each time. It can keep private data in a closure, or pick which object to return.
- A singleton is one shared object. Use it when sharing is the goal, and pass things in as arguments when tests need to swap them.
- A builder makes something in steps and checks it in
build(). An options object with defaults is the simpler choice. - Ways of copying differ in how deep they go and whether they keep the prototype. Check with real
===andinstanceoftests.
Remember the one-liner.
Pick the simplest way to make objects that keeps it clear what is new, what is shared, what is checked, and what is copied.
Up next: Observer and pub/sub shows how one part of an app can react when another part changes, without the two knowing about each other. Later lessons cover more patterns, app state, and how to organize a whole app.