Composite
Накратко: Composite дава на единичния елемент и на групата една и съща форма, затова дърво от продукти и кутии се обработва с едно извикване, на колкото и нива да е.
Проблем
Кутия в кутия в кутия
Магазинът продава подаръчни кутии. В коледната кутия има чай, мед и по-малка кутийка с бонбони и шоколад. Всяка кутия има и цена на опаковката. В поръчката има свещ и коледната кутия. Данните са такива:
class Product { constructor(name, price) { this.name = name; this.price = price; }}
class Box { constructor(name, packaging, items) { this.name = name; this.packaging = packaging; this.items = items; }}Сумата се смята с проверка какво е всеки предмет: продукт или кутия. За кутия има втори цикъл:
function total(order) { let sum = 0; for (const item of order) { if (item instanceof Product) { sum += item.price; } else if (item instanceof Box) { sum += item.packaging; for (const inner of item.items) { if (inner instanceof Product) { sum += inner.price; } } } } return sum;}const order = [ new Product("Свещ", 5), new Box("Коледна кутия", 2, [ new Product("Чай", 4.5), new Product("Мед", 6), new Box("Кутийка", 1, [ new Product("Бонбони", 3.5), new Product("Шоколад", 2.5)])])];console.log(`Общо: ${total(order).toFixed(2)} €`);Общо: 17.50 €Поръчката струва 24,50 €, а програмата казва 17,50 €. Кутийката е на третото ниво. Вторият цикъл я вижда, но тя не е продукт, и тя изчезва от сумата заедно с бонбоните и шоколада. Грешка няма.
Трети вложен цикъл поправя тази поръчка, но не и кутия на четвърто ниво. А същата верига от проверки има и в броенето на продуктите, и в теглото за куриера.
Идеята
Composite: частта и цялото с една форма
Продуктът и кутията се питат за едно и също: колко струваш? Продуктът отговаря с цената си. Кутията отговаря с опаковката плюс отговорите на всичко вътре.
Затова двете имат един и същ методtotal(). Кутията пази списък от предмети и не пита какви са: вика total() на всеки. Интерфейс не трябва, стига всеки предмет да има този метод.Това е шаблонът Composite (композиция). Има три роли:
- общата форма: методът
total(); - листото: продуктът, който смята сам;
- контейнерът: кутията, която пази деца със същата форма и пита всяко от тях.
Кутия в кутията е просто дете, което също пита децата си. Така извикванията слизат до продуктите, колкото и нива да има: това е рекурсия през обекти. Кодът, който пита за сумата, не знае дали държи продукт, кутия или цяло дърво.
Същата форма има и папката с файлове: размерът на файла е неговият, а размерът на папката е сборът на всичко в нея, включително на папките вътре.
Виж го
Сумата слиза до дъното и се връща
Продуктът и кутията. Кутията приема произволен брой предмети (...items):
class Product { constructor(name, price) { this.name = name; this.price = price; }
total() { return this.price; }}
class Box { constructor(name, packaging, ...items) { this.name = name; this.packaging = packaging; this.items = items; }
total() { let sum = this.packaging; for (const item of this.items) { sum += item.total(); } return sum; }}Дървото се пише като вложени конструктори, по един предмет на ред:
const order = new Box("Поръчка", 0, new Product("Свещ", 5), new Box("Коледна кутия", 2, new Product("Чай", 4.5), new Product("Мед", 6), new Box("Кутийка", 1, new Product("Бонбони", 3.5), new Product("Шоколад", 2.5))));console.log(`Общо: ${order.total().toFixed(2)} €`);Общо: 24.50 €const order = new Box("Поръчка", 0, new Product("Свещ", 5), new Box("Коледна кутия", 2, new Product("Чай", 4.5), new Product("Мед", 6), new Box("Кутийка", 1, new Product("Бонбони", 3.5), new Product("Шоколад", 2.5))));console.log(`Общо: ${order.total().toFixed(2)} €`);Стъпка по стъпка, с думи
- Дървото се сглобява: всеки предмет е на свой ред, а всяка кутия държи децата си.
- total() пита корена. Поръчката не знае какво има вътре: започва от опаковката си и вика total() на всяко дете.
- Свещ е продукт: отговаря с цената си, 5.00, и отговорът се качва обратно.
- Коледна кутия е кутия: прави точно същото, първо опаковката, после дете по дете.
- Кутийка е на третото ниво. Тук нищо не изчезва: тя пита своите деца.
- Кутийка: 1.00 + 3.50 + 2.50 = 7.00. Сумата ѝ се качва при родителя.
- Коледна кутия: 2.00 + 4.50 + 6.00 + 7.00 = 19.50.
- Поръчка: 0.00 + 5.00 + 19.50 = 24.50. Това печата програмата.
Никъде няма проверка на типа и никъде няма брой нива. Кутията прави едно и също на всяко ниво: опаковката плюс сумата на децата.
Виж го
Нов вид предмет, без да пипаш кутията
Магазинът започва да продава кафе на зърна по 24 € за кило. Цената му е цена за кило по килограми. Това е нов клас със същата форма:
class WeighedProduct { constructor(name, perKg, kg) { this.name = name; this.perKg = perKg; this.kg = kg; }
total() { return this.perKg * this.kg; }}const coffee = new Box("Кутия с кафе", 1, new WeighedProduct("Кафе на зърна", 24, 0.25), new Product("Чаша", 8));console.log(`Кутия с кафе: ${coffee.total().toFixed(2)} €`);Кутия с кафе: 15.00 €В Box не е сменен нито ред. Кутията вика total() на кафето, както на всеки друг предмет.
Тук кутията приема всичко. Текст на мястото на продукт се слага без грешка, а програмата спира чак при сумата:
const box = new Box("Кутия", 1, "Чай");console.log(box.total());TypeError: item.total is not a functionТова все пак е по-добре от проблема, където такъв предмет тихо се пропускаше: тук грешката излиза веднага щом някой поиска сумата.
По-точно, за любопитните
Нов вид предмет е лесен: един клас. Нова операция е по-скъпа: ако освен сумата трябва и тегло за куриера, методът weight() се добавя във всеки клас. Composite е силен там, където видовете предмети растат, а операциите са малко.
Пробвай
Предвиди изхода
В кутията има празна кутия и свещ. Какво печата програмата?
const box = new Box("Кутия", 1, new Box("Празна", 0.5), new Product("Свещ", 5));console.log(`Кутия: ${box.total().toFixed(2)} €`);Пробвай
Намери грешката
Някой преписва кутията, за да е „по-сигурна“:
class Box { constructor(name, packaging, ...items) { this.name = name; this.packaging = packaging; this.items = items; }
total() { let sum = this.packaging; for (const item of this.items) { if (item instanceof Product) { sum += item.total(); } } return sum; }}const gift = new Box("Коледна кутия", 2, new Product("Чай", 4.5), new Product("Мед", 6), new Box("Кутийка", 1, new Product("Бонбони", 3.5), new Product("Шоколад", 2.5)));console.log(`Коледна кутия: ${gift.total().toFixed(2)} €`);Коледна кутия: 12.50 €Коледната кутия струва 19,50 €, а програмата казва 12,50 €. На кой ред е грешката?
Избери реда с грешката.
Проверката пуска само продуктите. Кутийката вътре не е продукт, затова тя, бонбоните и шоколадът (7 €) изчезват от сумата. Това е същата грешка като в проблема, само че скрита в кутията. Контейнерът в Composite не пита какво е детето: вика total() на всяко. Махни проверката и сумата е 19,50 €.
Идеята
Кога не ти трябва
Composite има цена: обща форма и рекурсия, които трябва да прочетеш, за да разбереш една сума. Не си струва, когато:
- нещата са в плосък списък. Количка без кутии е списък, а сумата е един цикъл;
- нивата са точно две и такива остават: поръчка с редове. Ред в поръчката не съдържа поръчка;
- операциите стават много, а видовете предмети са два. Всяка нова операция се пише във всеки клас. Тогава понякога е по-ясно една функция да обходи дървото с проверка на вида, но на едно място.
Струва си, когато части и цели се влагат едни в други на колкото нива дойде: кутии, папки, категории в каталог, менюта с подменюта, елементи на страница.
Упражнение
Касовата бележка
Реши задачата в редактора си.
Бележката като дърво. Добави метод print(indent = "") в продукта и в кутията. Продуктът печата името и цената си. Кутията печата името и общата си сума, а после вика print на всяко дете с два интервала повече. Отпечатай поръчката от картата „Сумата слиза до дъното и се връща“:
Поръчка: 24.50 € Свещ: 5.00 € Коледна кутия: 19.50 € Чай: 4.50 € Мед: 6.00 € Кутийка: 7.00 € Бонбони: 3.50 € Шоколад: 2.50 €Още задачи
- Колко продукта. Добави
count(): продуктът е 1, а кутията е сборът на децата си. Опаковката не е продукт. В поръчката са 5. - Папки и файлове. Направи файл с име и размер в KB и папка с файлове и папки. Отпечатай размера на всяка папка. Какво е общото с кутиите?
- Най-скъпият продукт. Намери цената на най-скъпия продукт в цялото дърво. Помисли какво връща празна кутия.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Изключения · Масиви: push, splice и sort · Данни, switch и разглобяване · Шаблони за дизайн
- Продължава в: Command
- Задачи по темата: Шаблони за дизайн