Lab
Chain of responsibility
// An expense report, and what one approver decides about it.
type Expense = { id: string; amount: number };
type Decision = 'pass' | 'approved';
type Approver = { name: string; handle: (expense: Expense) => Decision };
type Outcome = { status: string; by: string; checked: string[] };
// One person on the team's roster, as the HR system lists them.
type Member = { name: string; limit: number; away: boolean };
// How an approver decides: approve up to a limit, pass anything larger.
function approveUpTo(expense: Expense, limit: number): Decision {
return expense.amount <= limit ? 'approved' : 'pass';
}
// Offer the expense to each approver in order; the first decision that is
// not a pass ends it.
function routeExpense(expense: Expense, approvers: Approver[]): Outcome {
const checked: string[] = [];
for (const candidate of approvers) {
const decision = candidate.handle(expense);
checked.push(candidate.name);
if (decision !== 'pass') return { status: decision, by: candidate.name, checked };
}
return { status: 'unhandled', by: 'nobody', checked };
}
// One line per expense: what happened, and who saw it on the way.
function report(expense: Expense, outcome: Outcome): string {
const path = outcome.checked.join(' > ') || '-';
const result =
outcome.status === 'unhandled' ? 'unhandled' : `${outcome.status} by ${outcome.by}`;
return `${expense.id}: ${result}; checked ${path}`;
}
// Written for the platform team in March: Sam approves up to 100, Dana up to
// 1,000 and finance up to 10,000. Every team now submits through it.
const approvers: Approver[] = [
{ name: 'Sam', handle: (expense) => approveUpTo(expense, 100) },
{ name: 'Dana', handle: (expense) => approveUpTo(expense, 1000) },
{ name: 'finance', handle: (expense) => approveUpTo(expense, 10000) },
];
// Route one expense report, given the submitting team's roster.
function submit(expense: Expense, roster: Member[]): string {
return report(expense, routeExpense(expense, approvers));
}