Lab
Abstract factory
type TravelDate = { year: number; month: number; day: number };
type Booking = {
id: string;
locale: string;
cents: number;
date: TravelDate;
};
// The team's setting: the locale for customers whose own locale has no
// formats yet. The checker changes defaultLocale between cases.
const settings = { defaultLocale: 'en' };
// Two product roles: a price format and a date format.
interface MoneyFormat {
format(cents: number): string;
}
interface DateFormat {
format(date: TravelDate): string;
}
const twoDigits = (n: number) => String(n).padStart(2, '0');
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// "EUR 12.50"
class PointMoney implements MoneyFormat {
format(cents: number): string {
return `EUR ${Math.floor(cents / 100)}.${twoDigits(cents % 100)}`;
}
}
// "12,50 EUR"
class CommaMoney implements MoneyFormat {
format(cents: number): string {
return `${Math.floor(cents / 100)},${twoDigits(cents % 100)} EUR`;
}
}
// "17 Oct 2026"
class NamedMonthDates implements DateFormat {
format(date: TravelDate): string {
return `${date.day} ${months[date.month - 1]} ${date.year}`;
}
}
// "17.10.2026"
class DottedDates implements DateFormat {
format(date: TravelDate): string {
return `${twoDigits(date.day)}.${twoDigits(date.month)}.${date.year}`;
}
}
// Two callers, each choosing its formats itself.
function confirmationLine(booking: Booking): string {
const money = booking.locale === 'de' ? new CommaMoney() : new PointMoney();
const dates = booking.locale === 'de' ? new DottedDates() : new NamedMonthDates();
const price = money.format(booking.cents);
return `${booking.id} confirmed: ${price}, travelling ${dates.format(booking.date)}`;
}
function reminderLine(booking: Booking): string {
const money = booking.locale === 'de' ? new CommaMoney() : new PointMoney();
const dates = new NamedMonthDates();
const price = money.format(booking.cents);
return `${booking.id} reminder: pay ${price} by ${dates.format(booking.date)}`;
}