Skip to content
wannacode

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.90
def label(self, recipient):
return f"{recipient}, to the door"
class OfficeDelivery:
price = 3.90
def label(self, recipient):
return f"{recipient}, at a courier office"
class LockerDelivery:
price = 2.90
def label(self, recipient):
return f"{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:

def checkout(kind, items):
if kind == "address":
delivery = AddressDelivery()
elif kind == "office":
delivery = OfficeDelivery()
elif kind == "locker":
delivery = LockerDelivery()
else:
raise ValueError(kind)
return items + delivery.price
def preview_price(kind):
if kind == "address":
delivery = AddressDelivery()
elif kind == "office":
delivery = OfficeDelivery()
elif kind == "locker":
delivery = LockerDelivery()
else:
raise ValueError(kind)
return delivery.price
def print_label(kind, recipient):
if kind == "office":
delivery = OfficeDelivery()
else:
delivery = AddressDelivery()
return delivery.label(recipient)
print(f"Order: €{checkout("locker", 40):.2f}")
print(f"Preview: delivery €{preview_price("locker"):.2f}")
print("Label: " + print_label("locker", "Maria"))
Output
Order: €42.90
Preview: delivery €2.90
Label: Maria, to the door

Lockers 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 call the class and doesn't know the class: it gets an object that has price and label.

The roles:

  • the product: the common shape, price and label(recipient), which every class has;
  • the concrete products: AddressDelivery, OfficeDelivery, LockerDelivery;
  • the factory: create_delivery(kind), 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 Python the factory is a type → class table. A class makes objects by itself: AddressDelivery() is a new object, so the table keeps the classes themselves, and DELIVERIES[kind]() calls the right one.

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

DELIVERIES = {
"address": AddressDelivery,
"office": OfficeDelivery,
"locker": LockerDelivery,
}
def create_delivery(kind):
if kind not in DELIVERIES:
raise ValueError(f"Unknown delivery type: {kind}")
return DELIVERIES[kind]()

The checkout, the preview and the label no longer choose, they ask:

def checkout(kind, items):
return items + create_delivery(kind).price
def preview_price(kind):
return create_delivery(kind).price
def print_label(kind, recipient):
return create_delivery(kind).label(recipient)
print(f"Order: €{checkout("locker", 40):.2f}")
print(f"Preview: delivery €{preview_price("locker"):.2f}")
print("Label: " + print_label("locker", "Maria"))
Output
Order: €42.90
Preview: delivery €2.90
Label: Maria, at a locker

The 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 not DELIVERIES.get(kind)()? If the type is missing, get returns None, and None() gives TypeError: 'NoneType' object is not callable. The error doesn't say which type is unknown. The check if kind not in DELIVERIES with a clear message costs two lines.

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.00
def label(self, recipient):
return f"{recipient}, at the store"

and one line in the factory, highlighted here:

DELIVERIES = {
"address": AddressDelivery,
"office": OfficeDelivery,
"locker": LockerDelivery,
"pickup": PickupDelivery,
}
def create_delivery(kind):
if kind not in DELIVERIES:
raise ValueError(f"Unknown delivery type: {kind}")
return DELIVERIES[kind]()
home = create_delivery("address")
print(f"{home.label('Maria')}: €{home.price:.2f}")
locker = create_delivery("locker")
print(f"{locker.label('Maria')}: €{locker.price:.2f}")
pickup = create_delivery("pickup")
print(f"{pickup.label('Maria')}: €{pickup.price:.2f}")
Output
Maria, to the door: €5.90
Maria, at a locker: €2.90
Maria, at the store: €0.00

home = create_delivery("address")
print(f"{home.label('Maria')}: €{home.price:.2f}")
locker = create_delivery("locker")
print(f"{locker.label('Maria')}: €{locker.price:.2f}")
pickup = create_delivery("pickup")
print(f"{pickup.label('Maria')}: €{pickup.price:.2f}")
Step by step, in words
  1. create_delivery("address") gets only the text from the order. The factory looks for the mold that matches it.
  2. A new AddressDelivery comes out of the mold. The caller doesn't ask which class it is: it only uses price and label().
  3. The same factory, a different text: "locker" goes past the other molds and stops at LockerDelivery.
  4. The label says locker now: the class is picked in one place, and the checkout, the preview and the label all ask that place.
  5. A new type: the class PickupDelivery and one more line in the factory. The lines that call create_delivery don't change.
  6. "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 = create_delivery("drone")
print(delivery.label("Maria"))
except ValueError as e:
print(e)
Output
Unknown delivery type: drone

The 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[kind] gives a KeyError with the type, but the message is for the developer, not for the order:
def create_delivery(kind):
return DELIVERIES[kind]()
Error
KeyError: 'drone'

See 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:
def __init__(self):
self.lines = ["product;total"]
def add(self, product, total):
self.lines.append(f"{product};{total:.2f}")
def text(self):
return "\n".join(self.lines)
class HtmlFile:
def __init__(self):
self.rows = []
def add(self, product, total):
self.rows.append(f"<tr><td>{product}</td><td>{total:.2f}</td></tr>")
def text(self):
return "<table>\n" + "\n".join(self.rows) + "\n</table>"
The shared algorithm is in the base class. Which file to make is said by the class attribute file_class: a class is a value, and a subclass puts in a different one. Frameworks like Django do it this way too.
class ReportExporter:
file_class = CsvFile
def export(self, sales):
file = self.file_class()
overall = 0
for product, total in sales:
file.add(product, total)
overall += total
file.add("Total", overall)
return file.text()
class HtmlExporter(ReportExporter):
file_class = HtmlFile
sales = [("Coffee", 12.50), ("Tea", 8.00)]
print(ReportExporter().export(sales))
print(HtmlExporter().export(sales))
Output
product;total
Coffee;12.50
Tea;8.00
Total;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 (self.file_class()), 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: export(sales, HtmlFile).

Try it

Predict the output

The types come from a form on the site. What does the program print?

for kind in ["office", "Locker", "locker"]:
try:
print(create_delivery(kind).label("Maria"))
except ValueError as e:
print(e)
Answer

Try it

Find the bug

Another developer wrote the factory like this:

DELIVERIES = {
"office": OfficeDelivery,
"locker": LockerDelivery,
}
def create_delivery(kind):
return DELIVERIES.get(kind, AddressDelivery)()

The order comes from a partner's site, which sends the type with a capital letter:

delivery = create_delivery("Locker")
print("Label: " + delivery.label("Maria"))
print(f"Delivery: €{delivery.price:.2f}")
Output
Label: Maria, to the door
Delivery: €5.90

Maria 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.

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. 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 DeliveryFactory class 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":

Output
Maria, express to the door: €9.90

More 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

1. The shop adds drone delivery, type "drone". What do you write with Simple Factory?
2. The factory gets a type it does not know. What should it do?
3. When is Factory Method better than Simple Factory?
4. To save memory, the table keeps one ready object for each type, and the factory returns it. The object keeps the recipient in a field. What happens with two locker orders in a row?
5. When don't you need a factory?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·