Lab
Template method
type Item = { id: string; title: string };
type Rows = string[][];
type ImportReport = { outcome: string; row: number; trace: string[] };
// Where an import lands: the whole catalog is replaced at once.
class Catalog {
items: Item[] = [{ id: 'existing', title: 'Already here' }];
replace(items: Item[]): void {
this.items = items;
}
}
// The batch rule every feed shares, checked on the normalized rows.
function validate(items: Item[]): { outcome: string; row: number } {
for (let i = 0; i < items.length; i++) {
const { id, title } = items[i];
if (id === '' || id.includes(' ')) {
return { outcome: 'invalid-id', row: i + 1 };
}
if (title === '') return { outcome: 'empty-title', row: i + 1 };
}
return { outcome: 'valid', row: 0 };
}
abstract class ImportTemplate {
// A required step: each feed reads its own columns.
protected abstract decode(rows: Rows): Item[];
// A hook: the default keeps the values as they arrived.
protected normalize(items: Item[]): Item[] {
return items;
}
// The template method: it owns the order and where the run stops.
run(rows: Rows, catalog: Catalog): ImportReport {
const trace = ['decode'];
const decoded = this.decode(rows);
trace.push('normalize');
const candidate = this.normalize(decoded);
trace.push('validate');
const check = validate(candidate);
if (check.outcome !== 'valid') return { ...check, trace };
trace.push('commit');
catalog.replace(candidate);
return { outcome: 'imported', row: 0, trace };
}
}
// The partner feed: ID, then title.
class IdTitleImporter extends ImportTemplate {
protected decode(rows: Rows): Item[] {
return rows.map(([id, title]) => ({ id, title }));
}
}
// The publisher feed: title, then ID.
class TitleIdImporter extends ImportTemplate {
protected decode(rows: Rows): Item[] {
return rows.map(([title, id]) => ({ id, title }));
}
}
// The spreadsheet export: ID first, with spaces around its cells.
class TrimmedImporter extends IdTitleImporter {
protected normalize(items: Item[]): Item[] {
return items.map((item) => ({
id: item.id.trim(),
title: item.title.trim(),
}));
}
}
function show(report: ImportReport): string {
const where = report.row ? ` at row ${report.row}` : '';
return `${report.outcome}${where} (${report.trace.join(' > ')})`;
}
const catalog = new Catalog();
const partner = new IdTitleImporter();
const publisher = new TitleIdImporter();
const spreadsheet = new TrimmedImporter();
const first = partner.run([['intro', 'Introduction'], ['faq', 'FAQ']], catalog);
const second = publisher.run([['', 'terms']], catalog);
const third = spreadsheet.run([[' guide ', ' Guide ']], catalog);
const saved = catalog.items.map((item) => item.id).join(', ');
console.log(`first: ${show(first)}`);
console.log(`second: ${show(second)}`);
console.log(`third: ${show(third)}`);
console.log(`saved: ${saved}`);
TODO
Visual to come