Course content
Composite: Price a Catalog of Nested Product Bundles
Medium·Tagsdesign-patternscompositestructuraltreesrecursion
Problem Statement
The store's catalog has single products and product bundles. A bundle contains catalog items: products or other bundles, nested to any depth. A bundle's price is the sum of its children's prices with the bundle's own percentage discount applied on top; its shipping weight is the plain sum of its children's weights, and discounts never touch weight.
```java
interface CatalogItem {
double price(); // final price in dollars, after any bundle discounts
double weight(); // shipping weight in kg; discounts never apply to weight
}
```
`Product implements CatalogItem` is the leaf and is already implemented. Implement `Bundle`, the composite, so that single items and whole trees are priced through the same interface:
```java
class Bundle implements CatalogItem {
public Bundle(String name, double discountPercent);
public void add(CatalogItem item);
// Append a child. Products, bundles, and item types that do not
// exist yet must all nest freely, so the parameter stays CatalogItem.
public double price();
// Sum of children's price() values, times (1 - discountPercent / 100).
// An empty bundle costs 0.
public double weight();
// Plain sum of children's weight() values. No discount, ever.
// An empty bundle weighs 0.
}
```
With `Bundle` in place, callers never check types:
```java
Bundle artKit = new Bundle("Art Mini-Kit", 20);
artKit.add(new Product("Crayons", 4.00, 0.20));
artKit.add(new Product("Sketch Pad", 6.00, 0.30));
Bundle backToSchool = new Bundle("Back to School", 10);
backToSchool.add(new Product("Notebook", 5.00, 0.40));
backToSchool.add(artKit); // a bundle inside a bundle
double total = backToSchool.price(); // one call walks the whole tree
```
The validator runs three checks:
1. flat_bundle: a bundle of three products prices at the discounted subtotal (9.00 from a 10.00 subtotal at 10% off) and weighs the plain sum (1.00). An empty bundle prices and weighs 0.
2. nested_bundles: bundles nest two levels deep and each level applies its own discount to its own subtree: the inner kit comes to 11.20, the outer kit to 14.58. Weights sum straight through with no discount anywhere.
3. uniform_extension (hidden): `Bundle` is a `CatalogItem` (so bundles nest), does not extend `Product`, exposes `add(CatalogItem)`, and correctly aggregates a brand-new item type the validator defines itself.
The stub `Bundle` throws from every method, so it fails every check until implemented. Starter code and validators are available in Java, Python, and C++.
Examples
Example 1
Input
kit = Bundle("School Kit", 10); add Notebook 5.00/0.40kg, Pen Pack 3.00/0.10kg, Marker Set 2.00/0.50kg; kit.price(); kit.weight()Output
"price 9.00 (10.00 subtotal minus 10%), weight 1.00 (the discount never touches weight)"Why
The bundle sums its children's prices, applies its own discount to the subtotal, and sums weights with no discount.
Example 2
Input
artKit = Bundle("Art Mini-Kit", 20) holding Crayons 4.00 + Sketch Pad 6.00 + Refill Pack (2.50 + 1.50 at 0% off); backToSchool = Bundle("Back to School", 10) holding Notebook 5.00 + artKitOutput
"artKit.price() = 11.20; backToSchool.price() = 14.58"Why
Each bundle applies its own discount to its own subtree: (4 + 6 + 4) * 0.8 = 11.20, then (5 + 11.20) * 0.9 = 14.58.
Example 3
Input
order = Bundle("Laptop Order", 0); order.add(Product("Laptop", 10.00, 1.00)); order.add(WarrantyAddon()) // a CatalogItem type defined by the validatorOutput
"order.price() = 22.00; order.weight() = 1.00"Why
The bundle never learns the concrete type. Any class implementing CatalogItem aggregates correctly, which is the point of typing children as the component.
Constraints
- •Bundle implements (derives from) CatalogItem, so bundles can nest inside bundles.
- •Bundle stores its children typed as CatalogItem, never as Product or Bundle.
- •add accepts a CatalogItem: any leaf or composite, including types that do not exist yet.
- •price() returns the sum of the children's price() values times (1 - discountPercent / 100). An empty bundle costs 0.
- •weight() returns the plain sum of the children's weight() values. Discounts never apply to weight. An empty bundle weighs 0.
- •Bundle must not extend Product. Leaf and composite are siblings behind the component interface.
Hints
Stuck? Reveal a nudge toward the right pattern, one step at a time.
Hint 1
Bundle needs three fields: the name, the discount percent, and a list of children typed as the component interface (List<CatalogItem> in Java, a plain list in Python, std::vector<const CatalogItem*> in C++). The constructor stores the first two and starts the list empty.
Hint 2
add is one line: append the item to the children list. Do not branch on the item's concrete type; the whole pattern is that you cannot tell and do not care.
Hint 3
price() is a loop and a multiply: sum child.price() over the children, then return subtotal * (1 - discountPercent / 100.0). The recursion is already there, because a child's price() may itself loop over ITS children.
Hint 4
weight() is the same loop without the multiply. Resist reusing price()'s discounted result; weight gets its own honest recursion over the same children.
Hint 5
The empty bundle needs no special case: an empty loop sums to 0, and 0 times any discount factor is still 0. If you wrote an if for it, delete the if.
Hint 6
The hidden check adds a catalog item class the validator defines itself. Your Bundle passes automatically IF children are typed as CatalogItem everywhere; it fails if you stored Products and Bundles in separate lists or branched on concrete types.