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:
interface Delivery { double price();
String label(String recipient);}
class AddressDelivery implements Delivery { @Override public double price() { return 5.90; }
@Override public String label(String recipient) { return recipient + ", to the door"; }}
class OfficeDelivery implements Delivery { @Override public double price() { return 3.90; }
@Override public String label(String recipient) { return recipient + ", at a courier office"; }}
class LockerDelivery implements Delivery { @Override public double price() { return 2.90; }
@Override public String label(String 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:
static double checkout(String type, double items) { Delivery delivery = switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException(type); }; return items + delivery.price();}
static double previewPrice(String type) { Delivery delivery = switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException(type); }; return delivery.price();}
static String printLabel(String type, String recipient) { Delivery delivery = switch (type) { case "office" -> new OfficeDelivery(); default -> new AddressDelivery(); }; return delivery.label(recipient);}System.out.printf("Order: €%.2f%n", checkout("locker", 40));System.out.printf("Preview: delivery €%.2f%n", previewPrice("locker"));System.out.println("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 default 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 the interface.
The roles:
- the product: the common shape, the
Deliveryinterface; - the concrete products:
AddressDelivery,OfficeDelivery,LockerDelivery; - the factory:
Deliveries.create(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.
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
class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> throw new IllegalArgumentException("Unknown delivery type: " + type); }; }}The checkout, the preview and the label no longer choose, they ask:
static double checkout(String type, double items) { return items + Deliveries.create(type).price();}
static double previewPrice(String type) { return Deliveries.create(type).price();}
static String printLabel(String type, String recipient) { return Deliveries.create(type).label(recipient);}System.out.printf("Order: €%.2f%n", checkout("locker", 40));System.out.printf("Preview: delivery €%.2f%n", previewPrice("locker"));System.out.println("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
The factory can also be a table: Map<String, Supplier<Delivery>> with values like AddressDelivery::new. A table is handy when the types are added from outside (every module registers its own). A switch is clearer when all the types are known in one place, and the compiler checks that there is a default (see the card "Unknown type").
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 implements Delivery { @Override public double price() { return 0.00; }
@Override public String label(String recipient) { return recipient + ", at the store"; }}and one line in the factory, highlighted here:
class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); case "pickup" -> new PickupDelivery(); default -> throw new IllegalArgumentException("Unknown delivery type: " + type); }; }}Delivery home = Deliveries.create("address");System.out.printf("%s: €%.2f%n", home.label("Maria"), home.price());
Delivery locker = Deliveries.create("locker");System.out.printf("%s: €%.2f%n", locker.label("Maria"), locker.price());
Delivery pickup = Deliveries.create("pickup");System.out.printf("%s: €%.2f%n", pickup.label("Maria"), pickup.price());Maria, to the door: €5.90Maria, at a locker: €2.90Maria, at the store: €0.00Delivery home = Deliveries.create("address");System.out.printf("%s: €%.2f%n", home.label("Maria"), home.price());
Delivery locker = Deliveries.create("locker");System.out.printf("%s: €%.2f%n", locker.label("Maria"), locker.price());
Delivery pickup = Deliveries.create("pickup");System.out.printf("%s: €%.2f%n", pickup.label("Maria"), pickup.price());Step by step, in words
- Deliveries.create("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, but home has the type Delivery: the caller sees only 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 Deliveries.create 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 { Delivery delivery = Deliveries.create("drone"); System.out.println(delivery.label("Maria"));} catch (IllegalArgumentException e) { System.out.println(e.getMessage());}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 if the factory has nodefault? Java doesn't let you forget it: a switch expression has to cover every value, or the program doesn't compile:class Deliveries { public static Delivery create(String type) { return switch (type) { case "address" -> new AddressDelivery(); case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); }; }}error: the switch expression does not cover all possible input valuesSee 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:
interface ReportFile { void add(String product, double total);
String text();}
class CsvFile implements ReportFile { private final List<String> lines = new ArrayList<>(List.of("product;total"));
@Override public void add(String product, double total) { lines.add(product + ";" + String.format("%.2f", total)); }
@Override public String text() { return String.join("\n", lines); }}
class HtmlFile implements ReportFile { private final List<String> rows = new ArrayList<>();
@Override public void add(String product, double total) { rows.add("<tr><td>" + product + "</td><td>" + String.format("%.2f", total) + "</td></tr>"); }
@Override public String text() { return "<table>\n" + String.join("\n", rows) + "\n</table>"; }}record Sale(String product, double total) {}
abstract class ReportExporter { protected abstract ReportFile createFile();
public String export(List<Sale> sales) { ReportFile file = createFile(); double overall = 0; for (Sale sale : sales) { file.add(sale.product(), sale.total()); overall += sale.total(); } file.add("Total", overall); return file.text(); }}
class CsvExporter extends ReportExporter { @Override protected ReportFile createFile() { return new CsvFile(); }}
class HtmlExporter extends ReportExporter { @Override protected ReportFile createFile() { return new HtmlFile(); }}List<Sale> sales = List.of(new Sale("Coffee", 12.50), new Sale("Tea", 8.00));System.out.println(new CsvExporter().export(sales));System.out.println(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 (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 constructor as a Supplier<ReportFile>: new ReportExporter(CsvFile::new).
Try it
Predict the output
The types come from a form on the site. What does the program print?
String[] types = { "office", "Locker", "locker" };for (String type : types) { try { System.out.println(Deliveries.create(type).label("Maria")); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }}Try it
Find the bug
Another developer wrote the factory like this:
class Deliveries { public static Delivery create(String type) { return switch (type) { case "office" -> new OfficeDelivery(); case "locker" -> new LockerDelivery(); default -> new AddressDelivery(); }; }}The order comes from a partner's site, which sends the type with a capital letter:
Delivery delivery = Deliveries.create("Locker");System.out.println("Label: " + delivery.label("Maria"));System.out.printf("Delivery: €%.2f%n", delivery.price());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);
-
there is a factory for the factory.
DeliveryFactoryFactoryalmost always means one of the two is not needed; - 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: HashMap and HashSet · record, switch and patterns · Design patterns · Strategy
- Continues in: Builder · The shop: which pattern where
- Tasks on this topic: Design patterns