Factory Method и Simple Factory
Накратко: Factory събира на едно място решението кой клас да се създаде: Simple Factory държи избора на едно място, а Factory Method оставя подкласа да реши кой обект да направи.
Проблем
Три места знаят всеки вид доставка
Поръчката казва как стига до клиента: "address" (до адреса), "office" (в офис на куриера) или "locker" (в автомат). За всеки вид има клас със своя цена и свой етикет:
class AddressDelivery { price = 5.9;
label(recipient) { return `${recipient}, до адреса`; }}
class OfficeDelivery { price = 3.9;
label(recipient) { return `${recipient}, в офис на куриера`; }}
class LockerDelivery { price = 2.9;
label(recipient) { return `${recipient}, в автомат`; }}Три части от магазина имат нужда от доставката: касата, прегледът на цената и принтерът за етикети. Всяка сама решава кой клас да създаде:
function checkout(type, items) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return items + delivery.price;}
function previewPrice(type) { let delivery; if (type === "address") { delivery = new AddressDelivery(); } else if (type === "office") { delivery = new OfficeDelivery(); } else if (type === "locker") { delivery = new LockerDelivery(); } else { throw new Error(type); } return delivery.price;}
function printLabel(type, recipient) { let delivery; if (type === "office") { delivery = new OfficeDelivery(); } else { delivery = new AddressDelivery(); } return delivery.label(recipient);}console.log(`Поръчка: ${checkout("locker", 40).toFixed(2)} €`);console.log(`Преглед: доставка ${previewPrice("locker").toFixed(2)} €`);console.log("Етикет: " + printLabel("locker", "Мария"));Поръчка: 42.90 €Преглед: доставка 2.90 €Етикет: Мария, до адресаАвтоматите дойдоха миналия месец. Някой добави "locker" в касата и в прегледа. Принтерът за етикети е писан преди това и има else за адрес. Мария плати доставка до автомат, а куриерът тръгна към адреса ѝ.
Нов вид е три промени, и една се забравя. Изборът „кой клас за кой вид“ е копиран, а копията не знаят едно за друго.
Идеята
Simple Factory: new живее на едно място
Изборът „кой клас за кой вид“ е едно решение, затова е на едно място: фабрика. Тя получава вида и връща нов обект. Викащият не пише new и не знае класа: получава обект, който има price и label.
Ролите:
- продуктът: общата форма,
priceиlabel(recipient), които има всеки клас; - конкретните продукти:
AddressDelivery,OfficeDelivery,LockerDelivery; - фабриката:
createDelivery(type), единственото място, което знае кой клас за кой вид; - клиентите: касата, прегледът, етикетът. Те само питат.
Това се казва Simple Factory (проста фабрика). Тя не е от книгата на „бандата на четиримата“, но е най-честата форма. Factory Method от книгата е малко друго нещо и идва по-нататък в урока.
Едно правило: непознат вид е грешка, а не тихо подразбиране. Фабрика, която не познава вида, хвърля изключение с ясно съобщение. Иначе грешката не изчезва, а тръгва с куриера към грешния адрес.
В JavaScript фабриката е таблица вид → клас. Класът е стойност като всяка друга, затова таблицата пази самите класове, аnew Delivery() прави обект от този, който е взет.Таблицата „име → стратегия“ от урока Strategy е същото нещо: фабрика, която по текст дава обект.
Виж го
Една фабрика за трите места
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Непознат вид доставка: ${type}`); } return new Delivery();}Касата, прегледът и етикетът вече не избират, а питат:
function checkout(type, items) { return items + createDelivery(type).price;}
function previewPrice(type) { return createDelivery(type).price;}
function printLabel(type, recipient) { return createDelivery(type).label(recipient);}console.log(`Поръчка: ${checkout("locker", 40).toFixed(2)} €`);console.log(`Преглед: доставка ${previewPrice("locker").toFixed(2)} €`);console.log("Етикет: " + printLabel("locker", "Мария"));Поръчка: 42.90 €Преглед: доставка 2.90 €Етикет: Мария, в автоматЕтикетът е за автомат. Изборът е на едно място, затова не може да е верен в касата и грешен в етикета.
По-точно, за любопитните
Защо new Delivery(), а не просто Delivery()? Класът в JavaScript не може да се вика като функция. Без new програмата спира с:
TypeError: Class constructor AddressDelivery cannot be invoked without 'new'Затова таблицата може да пази и функции, които правят обекта: ["address", () => new AddressDelivery()]. Тогава фабриката вика make() без new.
Виж го
Нов вид: един клас и един ред
Магазинът добавя вземане от магазина: вид "pickup", без такса. Новото е един клас:
class PickupDelivery { price = 0;
label(recipient) { return `${recipient}, от магазина`; }}и един ред във фабриката, маркиран тук:
const deliveries = new Map([ ["address", AddressDelivery], ["office", OfficeDelivery], ["locker", LockerDelivery], ["pickup", PickupDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type); if (Delivery === undefined) { throw new Error(`Непознат вид доставка: ${type}`); } return new Delivery();}const home = createDelivery("address");console.log(`${home.label("Мария")}: ${home.price.toFixed(2)} €`);
const locker = createDelivery("locker");console.log(`${locker.label("Мария")}: ${locker.price.toFixed(2)} €`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("Мария")}: ${pickup.price.toFixed(2)} €`);Мария, до адреса: 5.90 €Мария, в автомат: 2.90 €Мария, от магазина: 0.00 €const home = createDelivery("address");console.log(`${home.label("Мария")}: ${home.price.toFixed(2)} €`);
const locker = createDelivery("locker");console.log(`${locker.label("Мария")}: ${locker.price.toFixed(2)} €`);
const pickup = createDelivery("pickup");console.log(`${pickup.label("Мария")}: ${pickup.price.toFixed(2)} €`);Стъпка по стъпка, с думи
- createDelivery("address") получава само текста от поръчката. Фабриката търси калъпа за него.
- От калъпа излиза нов AddressDelivery. Викащият не пита кой клас е: ползва само price и label().
- Същата фабрика, друг текст: "locker" минава покрай другите калъпи и спира на LockerDelivery.
- Етикетът вече е за автомат: класът се избира на едно място, а касата, прегледът и етикетът питат все него.
- Нов вид: класът PickupDelivery и един ред повече във фабриката. Редовете, които викат createDelivery, не се пипат.
- "pickup" минава като всеки друг вид. Новият вид е добавен на едно място.
Касата, прегледът и етикетът не се пипат. Те разбират за новия вид, когато поръчка го поиска.
Виж го
Непознат вид
Видът идва отвън: от формуляр, от сайт на партньор, от стар файл. Рано или късно идва вид, който го няма:
try { const delivery = createDelivery("drone"); console.log(delivery.label("Мария"));} catch (e) { console.log(e.message);}Непознат вид доставка: droneГрешката казва кой вид е непознат. Хваща се там, където поръчката влиза в магазина, и клиентът вижда съобщение, вместо пратка на грешното място.
А без проверката?deliveries.get(type) връща undefined, а грешката не казва нито кой вид, нито какво е търсено:function createDelivery(type) { const Delivery = deliveries.get(type); return new Delivery();}TypeError: Delivery is not a constructorВиж го
Factory Method: подкласът решава
Simple Factory избира по текст. Понякога изборът не е по текст, а по това кой обект пита.
Отчетът за продажбите се праща като CSV (за счетоводството) и като HTML таблица (в имейла). Двата правят едно и също: всеки продукт е ред, а накрая има ред „Общо“. Различен е само файлът, в който се пишат редовете:
class CsvFile { lines = ["продукт;сума"];
add(product, total) { this.lines.push(`${product};${total.toFixed(2)}`); }
text() { return this.lines.join("\n"); }}
class HtmlFile { rows = [];
add(product, total) { this.rows.push(`<tr><td>${product}</td><td>${total.toFixed(2)}</td></tr>`); }
text() { return "<table>\n" + this.rows.join("\n") + "\n</table>"; }}createFile, който подкласът презаписва.class ReportExporter { createFile() { return new CsvFile(); }
export(sales) { const file = this.createFile(); let overall = 0; for (const [product, total] of sales) { file.add(product, total); overall += total; } file.add("Общо", overall); return file.text(); }}
class HtmlExporter extends ReportExporter { createFile() { return new HtmlFile(); }}const sales = [["Кафе", 12.5], ["Чай", 8]];console.log(new ReportExporter().export(sales));console.log(new HtmlExporter().export(sales));продукт;сумаКафе;12.50Чай;8.00Общо;20.50<table><tr><td>Кафе</td><td>12.50</td></tr><tr><td>Чай</td><td>8.00</td></tr><tr><td>Общо</td><td>20.50</td></tr></table>Това е Factory Method: базовият клас вика метод, който създава обекта (this.createFile()), а кой клас точно, решава подкласът.
Защо не се подава готов файл? Файлът събира редове. Всяко export трябва да започне с празен файл, затова отчетът не пази файл, а знае как да направи нов.
Кога Simple Factory стига? Factory Method си заслужава, когато около създаването има общ алгоритъм, който подкласовете не трябва да повтарят (тук: редовете и общата сума). Ако искаш само обект по вид, без алгоритъм около него, Simple Factory е по-ясна. А подклас, който съществува само за да върне нов обект, е церемония. Тогава подай самия клас: new ReportExporter(HtmlFile).
Пробвай
Предвиди изхода
Видовете идват от формуляр на сайта. Какво печата програмата?
for (const type of ["office", "Locker", "locker"]) { try { console.log(createDelivery(type).label("Мария")); } catch (e) { console.log(e.message); }}Пробвай
Намери грешката
Друг програмист е написал фабриката така:
const deliveries = new Map([ ["office", OfficeDelivery], ["locker", LockerDelivery],]);
function createDelivery(type) { const Delivery = deliveries.get(type) ?? AddressDelivery; return new Delivery();}Поръчката идва от сайт на партньор, който праща вида с главна буква:
const delivery = createDelivery("Locker");console.log("Етикет: " + delivery.label("Мария"));console.log(`Доставка: ${delivery.price.toFixed(2)} €`);Етикет: Мария, до адресаДоставка: 5.90 €Мария избра автомат. Плати доставка 5.90 € вместо 2.90 €, а пратката тръгна към адреса ѝ. На кой ред е грешката?
Избери реда с грешката.
Този ред тихо превръща всеки непознат вид в доставка до адрес. Главната буква е само поводът: утре ще е "lockr" или нов вид, който някой е забравил тук. Фабриката трябва да хвърля ясна грешка за непознат вид. Ако искаш да приемаш и "Locker", сведи вида до малки букви в самата фабрика, на едно място, а не във всеки, който я вика.
Идеята
Кога не ти трябва
Фабриката е още един скок при четене: за да видиш кой клас се създава, отиваш другаде. Не си струва, когато:
- на това място винаги трябва един и същ клас.
new OfficeDelivery()там, където доставката винаги е до офис, е по-ясно от фабрика; - видът е известен, докато пишеш кода, а не идва като данни (от поръчка, файл, формуляр);
-
пишеш клас
DeliveryFactoryсамо със статичен метод. Таблица и една функция стигат; - Factory Method има един подклас „за бъдещето“.
Струва си, когато видът идва като данни, а изборът на класа иначе би се копирал на няколко места. Или когато около създаването има общ алгоритъм, а различна е само стъпката „кой обект“.
Упражнение
Експресна доставка
Реши задачата в редактора си.
Експресна доставка. Магазинът добавя експресна доставка до адреса: вид "express", 9.90 €, етикет „…, експресно до адреса“. Добави клас ExpressDelivery и го направи достъпен през фабриката. Касата, прегледът и етикетът не се пипат. Създай експресна доставка за Мария и я отпечатай като в картата „Нов вид“:
Мария, експресно до адреса: 9.90 €Още задачи
- Главни букви и интервали. Формулярът праща
"Locker"и" locker ". Направи фабриката да ги приема, като промениш само нея. Непознатите видове пак трябва да дават грешка. - JSON отчет. Добави към отчета от картата „Factory Method“ формат JSON:
[{"product": "Кафе", "total": 12.50}, …]. Кои класове пишеш и кои не пипаш? - Цена според теглото. Автоматите приемат пратки до 20 кг, а над 5 кг доставката до адрес струва повече. Къде отива теглото: във фабриката или в метода за цена? Помисли защо.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Map и Set · Данни, switch и разглобяване · Шаблони за дизайн · Strategy
- Продължава в: Builder · Магазинът: кой шаблон къде
- Задачи по темата: Шаблони за дизайн