Builder
Накратко: Builder сглобява сложен обект стъпка по стъпка, с име за всяко поле, и го проверява в build(); в JavaScript същото често решава обект с опции.
Проблем
Девет параметъра и две разменени думи
Магазинът праща имейл, когато поръчката тръгне. Писмото има девет полета: до кого, на кой адрес да се отговаря, тема, текст, копие, скрито копие, прикачен файл, дали е важно и дали се следи отварянето.
class Email { constructor(to, replyTo, subject, body, cc, bcc, attachment, highPriority, trackOpens) { this.to = to; this.replyTo = replyTo; this.subject = subject; this.body = body; this.cc = cc; this.bcc = bcc; this.attachment = attachment; this.highPriority = highPriority; this.trackOpens = trackOpens; }
send() { console.log(`До: ${this.to}`); if (this.replyTo) { console.log(`Отговор до: ${this.replyTo}`); } console.log(`Тема: ${this.subject}`); if (this.attachment) { console.log(`Прикачено: ${this.attachment}`); } if (this.highPriority) { console.log("Важно"); } }}Ето как се праща потвърждението за поръчка 1042:
const email = new Email("support@shop.example", "maria@example.com", "Поръчка 1042 е изпратена", "Пратката е на път.", null, null, "invoice-1042.pdf", true, false);email.send();До: support@shop.exampleОтговор до: maria@example.comТема: Поръчка 1042 е изпратенаПрикачено: invoice-1042.pdfВажноПисмото отиде до поддръжката на магазина, а Мария не получи нищо. Първите два аргумента са разменени. И двата са текст, затова езикът няма как да разбере. А кое е true и кое false в края? За да разбереш, броиш позиции в конструктора. Два null пазят местата на полета, които това писмо няма.
Всяко ново поле, например езикът на писмото, сменя конструктора и всяко място, което го вика.
Идеята
Builder: по име, на стъпки, с проверка
Обектът не се прави с едно извикване на девет позиции, а на стъпки, всяка с име: .to(…), .subject(…). Стъпките ги приема отделен обект, builder. Той събира стойностите, а накрая build():
- проверява задължителните полета (тук: получател, тема и текст) и хвърля изключение, ако някое липсва;
- прави неизменим обект: след това писмото не може да се промени.
Това е шаблонът Builder (строител). Ролите:
- продуктът:
Email, неизменим; - builder-ът:
EmailBuilder, с метод за всяко поле. Методът връща самия builder, затова извикванията се нижат едно след друго във верига; build(): единственото място, където продуктът се проверява и се прави.
Виж го
Веригата и build()
class Email { constructor({ to, replyTo = null, subject, body, cc = null, bcc = null, attachment = null, highPriority = false, trackOpens = false, }) { Object.assign(this, { to, replyTo, subject, body, cc, bcc, attachment, highPriority, trackOpens }); Object.freeze(this); }
send() { console.log(`До: ${this.to}`); if (this.replyTo) { console.log(`Отговор до: ${this.replyTo}`); } console.log(`Тема: ${this.subject}`); if (this.attachment) { console.log(`Прикачено: ${this.attachment}`); } if (this.highPriority) { console.log("Важно"); } }}
class EmailBuilder { fields = {};
to(value) { this.fields.to = value; return this; }
replyTo(value) { this.fields.replyTo = value; return this; }
subject(value) { this.fields.subject = value; return this; }
body(value) { this.fields.body = value; return this; }
cc(value) { this.fields.cc = value; return this; }
bcc(value) { this.fields.bcc = value; return this; }
attachment(value) { this.fields.attachment = value; return this; }
highPriority() { this.fields.highPriority = true; return this; }
trackOpens() { this.fields.trackOpens = true; return this; }
build() { if (this.fields.to === undefined) { throw new Error("Липсва получател"); } if (this.fields.subject === undefined) { throw new Error("Липсва тема"); } if (this.fields.body === undefined) { throw new Error("Липсва текст"); } return new Email(this.fields); }}Builder-ът струва код: всяко поле е написано и в писмото, и в метода на builder-а. Затова не се прави за всеки клас.
Email приема един обект с опции и го разглобява по име, със стойности по подразбиране. Object.freeze(this) забранява промяната след създаването.const email = new EmailBuilder() .to("maria@example.com") .subject("Поръчка 1042 е изпратена") .body("Пратката е на път.") .attachment("invoice-1042.pdf") .highPriority() .build();email.send();
const draft = new EmailBuilder() .to("peter@example.com") .body("Пратката е на път.");try { draft.build();} catch (e) { console.log(e.message);}До: maria@example.comТема: Поръчка 1042 е изпратенаПрикачено: invoice-1042.pdfВажноЛипсва темаconst email = new EmailBuilder() .to("maria@example.com") .subject("Поръчка 1042 е изпратена") .body("Пратката е на път.") .attachment("invoice-1042.pdf") .highPriority() .build();email.send();
const draft = new EmailBuilder() .to("peter@example.com") .body("Пратката е на път.");try { draft.build();} catch (e) { console.log(e.message);}Стъпка по стъпка, с думи
- new EmailBuilder(): празен чертеж с девет гнезда. Трите задължителни (to, subject, body) са отбелязани.
- .to(…) пълни едно гнездо и връща същия builder, затова веригата продължава.
- Всяко извикване има име: стойността не може да отиде в чуждо гнездо, както ставаше с позициите в конструктора.
- .body(…): и трите задължителни гнезда са пълни.
- Незадължителните полета се пишат само ако трябват. highPriority() е без аргумент: вместо true на осма позиция.
- build() проверява задължителните гнезда и щампова Email. Полетата му са Object.freeze: обектът вече не се променя.
- Писмото тръгва към Мария, с фактурата, като важно.
- Нов builder, нов празен чертеж. Тук някой е забравил темата.
- draft.build() стига до празното гнездо subject и спира с изключение. Писмо без тема изобщо не се появява.
- Грешката казва какво липсва. Нищо не е изпратено.
Писмото без тема не стига до пощата: build() спира още при сглобяването.
Виж го
Без Builder: какво има езикът
class Email { constructor({ to, replyTo = null, subject, body, cc = null, bcc = null, attachment = null, highPriority = false, trackOpens = false, }) { if (!to || !subject || !body) { throw new Error("Липсва получател, тема или текст"); } Object.assign(this, { to, replyTo, subject, body, cc, bcc, attachment, highPriority, trackOpens }); Object.freeze(this); }}const email = new Email({ to: "maria@example.com", subject: "Поръчка 1042 е изпратена", body: "Пратката е на път.", attachment: "invoice-1042.pdf", highPriority: true,});console.log(email.to, "/", email.subject, "/", email.highPriority);maria@example.com / Поръчка 1042 е изпратена / trueObject.freeze спира промените. В модул (файл с import) или с "use strict" присвояването хвърля грешка, а в обикновен скрипт тихо не прави нищо:email.to = "peter@example.com";TypeError: Cannot assign to read only property 'to' of object '#<Email>'Кога Builder още си струва:
- писмото се сглобява на стъпки, в различни части на кода: прикаченият файл се добавя само ако има фактура, копието само за фирми;
- има проверка между полетата: например скритото копие не може да е адресът на получателя;
- полетата са много, повечето са незадължителни, а сглобяването трябва да се подаде от едно място на друго, преди да е готово.
Пробвай
Предвиди изхода
Един builder, две извиквания на build(). Какво печата програмата?
const builder = new EmailBuilder() .to("maria@example.com") .subject("Поръчка 1042 е изпратена") .body("Пратката е на път.");const first = builder.build();builder.highPriority();const second = builder.build();console.log(first.highPriority, second.highPriority);Пробвай
Намери грешката
Магазинът праща писмата за изпратените поръчки в цикъл. Фактура има само първата поръчка:
const orders = [ { email: "maria@example.com", id: 1042, invoice: "invoice-1042.pdf" }, { email: "peter@example.com", id: 1043, invoice: null },];const builder = new EmailBuilder().replyTo("support@shop.example");for (const order of orders) { builder .to(order.email) .subject(`Поръчка ${order.id} е изпратена`) .body("Пратката е на път."); if (order.invoice) { builder.attachment(order.invoice); } builder.build().send();}До: maria@example.comОтговор до: support@shop.exampleТема: Поръчка 1042 е изпратенаПрикачено: invoice-1042.pdfДо: peter@example.comОтговор до: support@shop.exampleТема: Поръчка 1043 е изпратенаПрикачено: invoice-1042.pdfПетър получи фактурата на Мария. На кой ред е грешката?
Избери реда с грешката.
Builder-ът е направен веднъж, преди цикъла. Той помни всичко, което му е казано, и прикаченият файл от първото писмо остава за второто, защото никой не го е изчистил. Builder-ът е за едно сглобяване: направи нов в цикъла, при всяко писмо. Тук грешката изтича лични данни: чужда фактура с име и адрес.
Идеята
Кога не ти трябва
Builder удвоява кода на класа. Не си струва, когато:
- полетата са две или три и всички са задължителни: обикновеният конструктор е по-ясен;
- обект с опции върши същото без втори клас;
- обектът няма какво да проверява и не се сглобява на стъпки.
Струва си, когато полетата са много и повечето са незадължителни, когато има проверка между тях, или когато обектът се сглобява на стъпки, в различни части на кода.
Упражнение
Пратка за куриер
Реши задачата в редактора си.
Builder за пратка. Куриерът иска пратка с получател, телефон, адрес и тегло (задължителни), а по желание: чупливо и наложен платеж. Напиши Shipment и builder за нея. build() проверява, че задължителните полета са зададени и че теглото е над 0 и до 30 кг. Направи пратка за Мария (2.5 кг, чупливо, наложен платеж 42.90 €), отпечатай я и опитай пратка от 45 кг:
Пратка за Мария, 0888 123 456Адрес: ул. Шипка 5, ПловдивТегло: 2.5 кг, чупливоНаложен платеж: 42.90 €Теглото трябва да е над 0 и до 30 кгОще задачи
- Поправи цикъла. Поправи програмата от „Намери грешката“ така, че всяко писмо да носи само своята фактура. Адресът за отговор трябва да остане за всички писма.
- Копие с една промяна. Добави метод, който от готово писмо прави builder със същите стойности (в Java често се казва
toBuilder()). Изпрати същото писмо до друг получател. - Начинът на езика. Напиши
Shipmentкато в картата „Без Builder“ и сравни двата варианта. Кой е по-кратък и какво губиш?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Вложени класове и данни · Данни, switch и разглобяване · Шаблони за дизайн · Factory Method и Simple Factory
- Продължава в: Магазинът: кой шаблон къде
- Задачи по темата: Шаблони за дизайн