Card 04/ 09
Visitor Example: Totals and Exports Over an Order's Line Items
Products, services and discounts are visited once for the total and once for an invoice.
An order made of different kinds of line item is a natural Visitor, and this example maps the template onto it. An order holds products, services and discount codes. The total and the printed invoice each need different code for each kind of line, and each becomes a visitor.
| Template | Order example |
|---|---|
| Visitor | LineVisitor |
| Concrete Element | Product, Service, Discount |
| Concrete Visitor | TotalVisitor, InvoiceVisitor |
| Client | The loop over the order's lines |
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
class LineVisitor(ABC):
@abstractmethod
def visit_product(self, line: Product) -> None: ...
@abstractmethod
def visit_service(self, line: Service) -> None: ...
@abstractmethod
def visit_discount(self, line: Discount) -> None: ...
@dataclass(frozen=True)
class Product:
name: str
unit_pence: int
quantity: int
def accept(self, visitor: LineVisitor) -> None:
visitor.visit_product(self)
@dataclass(frozen=True)
class Service:
name: str
hourly_pence: int
hours: int
def accept(self, visitor: LineVisitor) -> None:
visitor.visit_service(self)
@dataclass(frozen=True)
class Discount:
code: str
pence: int
def accept(self, visitor: LineVisitor) -> None:
visitor.visit_discount(self)
class TotalVisitor(LineVisitor):
def __init__(self) -> None:
self.pence = 0
def visit_product(self, line: Product) -> None:
self.pence += line.unit_pence * line.quantity
def visit_service(self, line: Service) -> None:
self.pence += line.hourly_pence * line.hours
def visit_discount(self, line: Discount) -> None:
self.pence -= line.pence
class InvoiceVisitor(LineVisitor):
def __init__(self) -> None:
self.rows: list[str] = []
def visit_product(self, line: Product) -> None:
self.rows.append(f"{line.quantity} x {line.name:<14} £{line.unit_pence * line.quantity / 100:>7.2f}")
def visit_service(self, line: Service) -> None:
self.rows.append(f"{line.hours} h {line.name:<14} £{line.hourly_pence * line.hours / 100:>7.2f}")
def visit_discount(self, line: Discount) -> None:
self.rows.append(f"code {line.code:<14}-£{line.pence / 100:>7.2f}")
order = [
Product("desk lamp", 2499, 2),
Service("installation", 4000, 1),
Discount("WELCOME10", 1000),
]
total = TotalVisitor()
invoice = InvoiceVisitor()
for line in order:
line.accept(total)
line.accept(invoice)
print("\n".join(invoice.rows))
print(f"total: £{total.pence / 100:.2f}")2 x desk lamp £ 49.98
1 h installation £ 40.00
code WELCOME10 -£ 10.00
total: £79.98Two lamps at £24.99, an hour of installation at £40 and a £10 code give a total of £79.98, and the invoice lists the same three lines with their amounts. Each visitor keeps its own state, the running total or the list of rows, between visits.
Everything about pricing is in TotalVisitor, and everything about layout is in InvoiceVisitor. A third operation, such as tax by line type, is one new class, and the three line classes do not change. A new kind of line, such as a gift card, is the costly direction, since every visitor must learn it.
← → move through sections and cards