Lab
Null object
type TraceLine = { label: string; cents: number };
// A trace accepts explanation lines. It does not promise to keep them.
interface QuoteTrace {
note(line: TraceLine): void;
}
// Keeps every line, for the "why this price?" view.
class RecordingTrace implements QuoteTrace {
lines: TraceLine[] = [];
note(line: TraceLine): void {
this.lines.push(line);
}
}
// The null object: a real trace whose note keeps nothing.
const noTrace: QuoteTrace = {
note(): void {},
};
type Item = { name: string; unitCents: number; quantity: number };
type Quote = {
subtotal: number;
discount: number;
shipping: number;
tax: number;
total: number;
};
// Required: a rate for a known region, or an error. Never a made-up zero.
class MemoryRates {
percents = new Map([['north', 8], ['south', 5]]);
percentFor(region: string): number {
const percent = this.percents.get(region);
if (percent === undefined) throw new Error(`unknown-region-${region}`);
return percent;
}
}
// Pricing calls note at every step and never asks which trace it has.
function priceOrder(
items: Item[], code: string, region: string,
rates: MemoryRates, trace: QuoteTrace,
): Quote {
const percent = rates.percentFor(region);
let subtotal = 0;
for (const item of items) subtotal += item.unitCents * item.quantity;
trace.note({ label: 'subtotal', cents: subtotal });
const discount = code === 'WELCOME10' ? Math.floor(subtotal / 10) : 0;
trace.note({ label: `discount ${code}`, cents: discount });
const taxable = subtotal - discount;
const shipping = taxable >= 5000 ? 0 : 500;
trace.note({ label: 'shipping', cents: shipping });
const tax = Math.floor((taxable * percent + 50) / 100);
trace.note({ label: `tax ${region} ${percent}%`, cents: tax });
const total = taxable + shipping + tax;
trace.note({ label: 'total', cents: total });
return { subtotal, discount, shipping, tax, total };
}
const rates = new MemoryRates();
const items: Item[] = [
{ name: 'Notebook', unitCents: 1200, quantity: 2 },
{ name: 'Pen', unitCents: 300, quantity: 4 },
];
// The "why this price?" view explains; the invoice batch does not.
const recording = new RecordingTrace();
const explained = priceOrder(items, 'WELCOME10', 'north', rates, recording);
const silent = priceOrder(items, 'WELCOME10', 'north', rates, noTrace);
const kept = recording.lines.length;
const sameTotal = explained.total === silent.total;
// A missing rate is a failure in either mode, not a zero.
let missing = '';
try {
priceOrder(items, 'WELCOME10', 'island', rates, noTrace);
} catch (error) {
missing = (error as Error).message;
}
console.log(`explained: total ${explained.total}, ${kept} lines kept`);
console.log(`silent: total ${silent.total}, same total: ${sameTotal}`);
console.log(`island: ${missing}`);
TODO
Visual to come