Composite
Накратко: Composite дава на единичния елемент и на групата една и съща форма, затова дърво от продукти и кутии се обработва с едно извикване, на колкото и нива да е.
Проблем
Кутия в кутия в кутия
Магазинът продава подаръчни кутии. В коледната кутия има чай, мед и по-малка кутийка с бонбони и шоколад. Всяка кутия има и цена на опаковката. В поръчката има свещ и коледната кутия. Данните са такива:
record Product(string Name, decimal Price);
record Box(string Name, decimal Packaging, List<object> Items);Сумата се смята с проверка какво е всеки предмет: продукт или кутия. За кутия има втори цикъл:
static decimal Total(List<object> order){ decimal sum = 0; foreach (object item in order) { if (item is Product p) { sum += p.Price; } else if (item is Box box) { sum += box.Packaging; foreach (object inner in box.Items) { if (inner is Product q) { sum += q.Price; } } } } return sum;}List<object> order = [ new Product("Свещ", 5.00m), new Box("Коледна кутия", 2.00m, [ new Product("Чай", 4.50m), new Product("Мед", 6.00m), new Box("Кутийка", 1.00m, [ new Product("Бонбони", 3.50m), new Product("Шоколад", 2.50m)])])];Console.WriteLine($"Общо: {Total(order):F2} €");Общо: 17.50 €Поръчката струва 24,50 €, а програмата казва 17,50 €. Кутийката е на третото ниво. Вторият цикъл я вижда, но тя не е продукт, и тя изчезва от сумата заедно с бонбоните и шоколада. Грешка няма.
Трети вложен цикъл поправя тази поръчка, но не и кутия на четвърто ниво. А същата верига от проверки има и в броенето на продуктите, и в теглото за куриера.
Идеята
Composite: частта и цялото с една форма
Продуктът и кутията се питат за едно и също: колко струваш? Продуктът отговаря с цената си. Кутията отговаря с опаковката плюс отговорите на всичко вътре.
Затова двете имат обща форма: интерфейсътItem (в C# IItem) с метод Total(). Кутията пази списък от Item и не знае кое в него е продукт и кое кутия.Това е шаблонът Composite (композиция). Има три роли:
- общата форма: интерфейсът с
Total(); - листото: продуктът, който смята сам;
- контейнерът: кутията, която пази деца със същата форма и пита всяко от тях.
Кутия в кутията е просто дете, което също пита децата си. Така извикванията слизат до продуктите, колкото и нива да има: това е рекурсия през обекти. Кодът, който пита за сумата, не знае дали държи продукт, кутия или цяло дърво.
Същата форма има и папката с файлове: размерът на файла е неговият, а размерът на папката е сборът на всичко в нея, включително на папките вътре.
Виж го
Сумата слиза до дъното и се връща
Общата форма:
interface IItem{ decimal Total();}Продуктът и кутията. Конструкторът на кутията приема произволен брой предмети (params IItem[] items):
record Product(string Name, decimal Price) : IItem{ public decimal Total() => Price;}
class Box : IItem{ private readonly List<IItem> items;
public string Name { get; } public decimal Packaging { get; }
public Box(string name, decimal packaging, params IItem[] items) { Name = name; Packaging = packaging; this.items = new List<IItem>(items); }
public decimal Total() { decimal sum = Packaging; foreach (IItem item in items) { sum += item.Total(); } return sum; }}Дървото се пише като вложени конструктори, по един предмет на ред:
IItem order = new Box("Поръчка", 0, new Product("Свещ", 5.00m), new Box("Коледна кутия", 2.00m, new Product("Чай", 4.50m), new Product("Мед", 6.00m), new Box("Кутийка", 1.00m, new Product("Бонбони", 3.50m), new Product("Шоколад", 2.50m))));Console.WriteLine($"Общо: {order.Total():F2} €");Общо: 24.50 €IItem order = new Box("Поръчка", 0, new Product("Свещ", 5.00m), new Box("Коледна кутия", 2.00m, new Product("Чай", 4.50m), new Product("Мед", 6.00m), new Box("Кутийка", 1.00m, new Product("Бонбони", 3.50m), new Product("Шоколад", 2.50m))));Console.WriteLine($"Общо: {order.Total():F2} €");Стъпка по стъпка, с думи
- Дървото се сглобява: всеки предмет е на свой ред, а всяка кутия държи децата си.
- 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 € за кило. Цената му е цена за кило по килограми. Това е нов клас със същата форма:
record WeighedProduct(string Name, decimal PerKg, decimal Kg) : IItem{ public decimal Total() => PerKg * Kg;}IItem coffee = new Box("Кутия с кафе", 1.00m, new WeighedProduct("Кафе на зърна", 24.00m, 0.25m), new Product("Чаша", 8.00m));Console.WriteLine($"Кутия с кафе: {coffee.Total():F2} €");Кутия с кафе: 15.00 €В Box не е сменен нито ред. Кутията вика Total() на кафето, както на всеки друг предмет.
В проблема списъкът беше List<object> и приемаше всичко. Сега кутията приема само IItem, и текст на мястото на продукт не се компилира:
IItem box = new Box("Кутия", 1.00m, "Чай");error CS1503: Argument 3: cannot convert from 'string' to 'IItem'По-точно, за любопитните
Нов вид предмет е лесен: един клас. Нова операция е по-скъпа: ако освен сумата трябва и тегло за куриера, методът Weight() се добавя във всеки клас. Composite е силен там, където видовете предмети растат, а операциите са малко.
Пробвай
Предвиди изхода
В кутията има празна кутия и свещ. Какво печата програмата?
IItem box = new Box("Кутия", 1.00m, new Box("Празна", 0.50m), new Product("Свещ", 5.00m));Console.WriteLine($"Кутия: {box.Total():F2} €");Пробвай
Намери грешката
Някой преписва кутията, за да е „по-сигурна“:
class Box : IItem{ private readonly List<IItem> items;
public string Name { get; } public decimal Packaging { get; }
public Box(string name, decimal packaging, params IItem[] items) { Name = name; Packaging = packaging; this.items = new List<IItem>(items); }
public decimal Total() { decimal sum = Packaging; foreach (IItem item in items) { if (item is Product) { sum += item.Total(); } } return sum; }}IItem gift = new Box("Коледна кутия", 2.00m, new Product("Чай", 4.50m), new Product("Мед", 6.00m), new Box("Кутийка", 1.00m, new Product("Бонбони", 3.50m), new Product("Шоколад", 2.50m)));Console.WriteLine($"Коледна кутия: {gift.Total():F2} €");Коледна кутия: 12.50 €Коледната кутия струва 19,50 €, а програмата казва 12,50 €. На кой ред е грешката?
Избери реда с грешката.
Проверката пуска само продуктите. Кутийката вътре не е продукт, затова тя, бонбоните и шоколадът (7 €) изчезват от сумата. Това е същата грешка като в проблема, само че скрита в кутията. Контейнерът в Composite не пита какво е детето: вика Total() на всяко. Махни проверката и сумата е 19,50 €.
Идеята
Кога не ти трябва
Composite има цена: обща форма и рекурсия, които трябва да прочетеш, за да разбереш една сума. Не си струва, когато:
- нещата са в плосък списък. Количка без кутии е списък, а сумата е един цикъл;
- нивата са точно две и такива остават: поръчка с редове. Ред в поръчката не съдържа поръчка;
- операциите стават много, а видовете предмети са два. Всяка нова операция се пише във всеки клас. Тогава понякога е по-ясно една функция да обходи дървото с проверка на вида, но на едно място.
Струва си, когато части и цели се влагат едни в други на колкото нива дойде: кутии, папки, категории в каталог, менюта с подменюта, елементи на страница.
Упражнение
Касовата бележка
Реши задачата в редактора си.
Бележката като дърво. Добави метод Print(string indent) в продукта и в кутията, а и в интерфейса. Продуктът печата името и цената си. Кутията печата името и общата си сума, а после вика Print на всяко дете с два интервала повече. Отпечатай поръчката от картата „Сумата слиза до дъното и се връща“:
Поръчка: 24.50 € Свещ: 5.00 € Коледна кутия: 19.50 € Чай: 4.50 € Мед: 6.00 € Кутийка: 7.00 € Бонбони: 3.50 € Шоколад: 2.50 €Още задачи
- Колко продукта. Добави
Count(): продуктът е 1, а кутията е сборът на децата си. Опаковката не е продукт. В поръчката са 5. - Папки и файлове. Направи файл с име и размер в KB и папка с файлове и папки. Отпечатай размера на всяка папка. Какво е общото с кутиите?
- Най-скъпият продукт. Намери цената на най-скъпия продукт в цялото дърво. Помисли какво връща празна кутия.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Изключения · Списъци · record, switch и шаблони · Шаблони за дизайн
- Продължава в: Command
- Задачи по темата: Шаблони за дизайн