Factory Method and Simple Factory
In short: A factory keeps the decision of which class to create in one place: Simple Factory holds the choice in one spot, and Factory Method lets a subclass decide which object to make.
Problem
Three places know every delivery type
The order says how it gets to the customer: "address" (to the door), "office" (at a courier office) or "locker" (at a parcel locker). Every type has a class with its own price and its own label:
class AddressDelivery { price = 5.9;
label(recipient) { return `${recipient}, to the door`; }}
class OfficeDelivery { price = 3.9;
label(recipient) { return `${recipient}, at a courier office`; }}
class LockerDelivery { price = 2.9;
label(recipient) { return `${recipient}, at a locker`; }}Three parts of the shop need the delivery: the checkout, the price preview and the label printer. Each one decides by itself which class to create:
function checkout(type, items) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return items + delivery.price;}
function previewPrice(type) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return delivery.price;}
function printLabel(type, recipient) { let delivery; if (type === "office") { delivery = new OfficeDelivery(); } else { delivery = new AddressDelivery(); } return delivery.label(recipient);}console.log(`Order: €${checkout("locker", 40).toFixed(2)}`);console.log(`Preview: delivery €${previewPrice("locker").toFixed(2)}`);console.log("Label: " + printLabel("locker", "Maria"));Order: €42.90Preview: delivery €2.90Label: Maria, to the doorLockers arrived last month. Someone added "locker" to the checkout and to the preview. The label printer was written before that and has an else for the address. Maria paid for delivery to a locker, and the courier went to her address.
A new type is three changes, and one gets forgotten. The choice "which class for which type" is copied, and the copies don't know about each other.
The idea
Simple Factory: new lives in one place
The choice "which class for which type" is one decision, so it lives in one place: a factory. It gets the type and returns a new object. The caller doesn't write new and doesn't know the class: it gets an object that has price and label.
The roles:
- the product: the common shape,
priceandlabel(recipient), which every class has; - the concrete products:
AddressDelivery,OfficeDelivery,LockerDelivery; - the factory:
createDelivery(type), the only place that knows which class goes with which type; - the clients: the checkout, the preview, the label. They only ask.
This is called Simple Factory. It is not from the Gang of Four book, but it is the most common form. Factory Method from the book is a slightly different thing and comes later in the lesson.
One rule: an unknown type is an error, not a silent default. A factory that doesn't know the type throws an exception with a clear message. Otherwise the error doesn't go away, it leaves with the courier for the wrong address.
In JavaScript the factory is a type → class table. A class is a value like any other, so the table keeps the classes themselves, andnew Delivery() makes an object from the one it took.The "name → strategy" table from the Strategy lesson is the same thing: a factory that gives you an object for a piece of text.
See it
One factory for the three places
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Unknown delivery type: ${type}`); } return new Delivery();}The checkout, the preview and the label no longer choose, they ask:
function checkout(type, items) { return items + createDelivery(type).price;}
function previewPrice(type) { return createDelivery(type).price;}
function printLabel(type, recipient) { return createDelivery(type).label(recipient);}console.log(`Order: €${checkout("locker", 40).toFixed(2)}`);console.log(`Preview: delivery €${previewPrice("locker").toFixed(2)}`);console.log("Label: " + printLabel("locker", "Maria"));Order: €42.90Preview: delivery €2.90Label: Maria, at a lockerThe label is for a locker. The choice is in one place, so it can't be right in the checkout and wrong on the label.
More precisely, if you are curious
Why new Delivery() and not just Delivery()? In JavaScript a class can't be called like a function. Without new the program stops with:
TypeError: Class constructor AddressDelivery cannot be invoked without 'new'That is why the table can also keep functions that make the object: ["address", () => new AddressDelivery()]. Then the factory calls make() without new.
See it
A new type: one class and one line
The shop adds store pickup: type "pickup", free of charge. What's new is one class:
class PickupDelivery { price = 0;
label(recipient) { return `${recipient}, at the store`; }}and one line in the factory, highlighted here:
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery], ["pickup", PickupDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Unknown delivery type: ${type}`); } return new Delivery();}const home = createDelivery("address");console.log(`${home.label("Maria")}: €${home.price.toFixed(2)}`);
const locker = createDelivery("locker");console.log(`${locker.label("Maria")}: €${locker.price.toFixed(2)}`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("Maria")}: €${pickup.price.toFixed(2)}`);Maria, to the door: €5.90Maria, at a locker: €2.90Maria, at the store: €0.00const home = createDelivery("address");console.log(`${home.label("Maria")}: €${home.price.toFixed(2)}`);
const locker = createDelivery("locker");console.log(`${locker.label("Maria")}: €${locker.price.toFixed(2)}`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("Maria")}: €${pickup.price.toFixed(2)}`);Step by step, in words
- createDelivery("address") gets only the text from the order. The factory looks for the mold that matches it.
- A new AddressDelivery comes out of the mold. The caller doesn't ask which class it is: it only uses price and label().
- The same factory, a different text: "locker" goes past the other molds and stops at LockerDelivery.
- The label says locker now: the class is picked in one place, and the checkout, the preview and the label all ask that place.
- A new type: the class PickupDelivery and one more line in the factory. The lines that call createDelivery don't change.
- "pickup" goes through like any other type. The new type was added in one place.
The checkout, the preview and the label are not touched. They learn about the new type when an order asks for it.
See it
Unknown type
The type comes from outside: from a form, from a partner's site, from an old file. Sooner or later a type shows up that isn't there:
try { const delivery = createDelivery("drone"); console.log(delivery.label("Maria"));} catch (e) { console.log(e.message);}Unknown delivery type: droneThe error says which type is unknown. It is caught where the order comes into the shop, and the customer sees a message instead of a parcel at the wrong place.
And without the check?deliveries.get(type) returns undefined, and the error says neither which type nor what was being looked up:function createDelivery(type) { const Delivery = deliveries.get(type); return new Delivery();}TypeError: Delivery is not a constructorSee it
Factory Method: the subclass decides
Simple Factory chooses by text. Sometimes the choice isn't by text, but by which object is asking.
The sales report is sent as CSV (for accounting) and as an HTML table (in an email). Both do the same thing: every product is a row, and at the end there is a "Total" row. Only the file the rows are written to is different:
class CsvFile { lines = ["product;total"];
add(product, total) { this.lines.push(`${product};${total.toFixed(2)}`); }
text() { return this.lines.join("\n"); }}
class HtmlFile { rows = [];
add(product, total) { this.rows.push(`<tr><td>${product}</td><td>${total.toFixed(2)}</td></tr>`); }
text() { return "<table>\n" + this.rows.join("\n") + "\n</table>"; }}createFile method, which the subclass overrides.class ReportExporter { createFile() { return new CsvFile(); }
export(sales) { const file = this.createFile(); let overall = 0; for (const [product, total] of sales) { file.add(product, total); overall += total; } file.add("Total", overall); return file.text(); }}
class HtmlExporter extends ReportExporter { createFile() { return new HtmlFile(); }}const sales = [["Coffee", 12.5], ["Tea", 8]];console.log(new ReportExporter().export(sales));console.log(new HtmlExporter().export(sales));product;totalCoffee;12.50Tea;8.00Total;20.50<table><tr><td>Coffee</td><td>12.50</td></tr><tr><td>Tea</td><td>8.00</td></tr><tr><td>Total</td><td>20.50</td></tr></table>This is Factory Method: the base class calls a method that creates the object (this.createFile()), and the subclass decides which class exactly.
Why not pass in a ready file? The file collects rows. Every export has to start with an empty file, so the report doesn't keep a file, it knows how to make a new one.
When is Simple Factory enough? Factory Method pays off when there is a shared algorithm around the creation that the subclasses shouldn't repeat (here: the rows and the total). If you only want an object by type, with no algorithm around it, Simple Factory is clearer. And a subclass that exists only to return a new object is ceremony. Then pass the class itself: new ReportExporter(HtmlFile).
Try it
Predict the output
The types come from a form on the site. What does the program print?
for (const type of ["office", "Locker", "locker"]) { try { console.log(createDelivery(type).label("Maria")); } catch (e) { console.log(e.message); }}Try it
Find the bug
Another developer wrote the factory like this:
const deliveries = new Map([ ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type) ?? AddressDelivery; return new Delivery();}The order comes from a partner's site, which sends the type with a capital letter:
const delivery = createDelivery("Locker");console.log("Label: " + delivery.label("Maria"));console.log(`Delivery: €${delivery.price.toFixed(2)}`);Label: Maria, to the doorDelivery: €5.90Maria chose a locker. She paid €5.90 for delivery instead of €2.90, and the parcel went to her address. Which line is the bug on?
Pick the line with the bug.
This line quietly turns every unknown type into a delivery to the door. The capital letter is only the trigger: tomorrow it will be "lockr", or a new type someone forgot to add here. The factory must throw a clear error for an unknown type. If you want to accept "Locker" too, turn the type into lowercase in the factory itself, in one place, and not in everyone who calls it.
The idea
When you don't need it
A factory is one more jump when you read: to see which class gets created, you go somewhere else. It isn't worth it when:
- that place always needs the same class.
new OfficeDelivery()where the delivery always goes to an office is clearer than a factory; - the type is known while you write the code, and doesn't come in as data (from an order, a file, a form);
-
you write a
DeliveryFactoryclass with only a static method. A table and one function are enough; - a Factory Method has one subclass "for the future".
It pays off when the type comes in as data, and the choice of the class would otherwise be copied in several places. Or when there is a shared algorithm around the creation, and only the step "which object" is different.
Exercise
Express delivery
Solve this in your editor.
Express delivery. The shop adds express delivery to the door: type "express", €9.90, label "…, express to the door". Add an ExpressDelivery class and make it available through the factory. The checkout, the preview and the label are not touched. Create an express delivery for Maria and print it like in the card "A new type":
Maria, express to the door: €9.90More tasks
- Capital letters and spaces. The form sends
"Locker"and" locker ". Make the factory accept them, changing only the factory. Unknown types must still give an error. - A JSON report. Add a JSON format to the report from the card "Factory Method":
[{"product": "Coffee", "total": 12.50}, …]. Which classes do you write, and which don't you touch? - Price by weight. Lockers take parcels up to 20 kg, and over 5 kg delivery to the door costs more. Where does the weight go: into the factory or into the price method? Think about why.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Map and Set · Data, switch and destructuring · Design patterns · Strategy
- Continues in: Builder · The shop: which pattern where
- Tasks on this topic: Design patterns