Bags Inside Pouches Inside the Inventory
Writing separate loops and type-checks for individual items versus containers (`if (isBag) ... else ...`) breaks recursive nesting. Naive designs force separate vectors for items and bags, duplicate traversal logic on every depth level, and fail to treat single items and composite pouches uniformly.
A Bag can only contain flat Items. If a player places an HerbPouch inside a LeatherBackpack, the naive model cannot represent it without constantly adding nested vector<Bag> subBags fields.
Compose objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions of objects uniformly.
We define an abstract InventoryNode interface declaring weight(), goldValue(), and print(). Leaf nodes (LootItem) return their own values directly. Composite nodes (ContainerBag) own a collection of children via std::vector<std::unique_ptr<InventoryNode>> and recursively aggregate values from their children. The root PlayerInventory interacts purely with InventoryNode, so a bag containing 50 pouches is evaluated with the exact same call as a single sword.
In Sprint 1 (C1 Β· Decorator), we implemented stackable weapon enchantments and hero buffs that wrap characters dynamically without subclass explosion. With buffed heroes fighting and collecting loot, our engine now needs to manage complex inventories and group structures.
We need to support recursive inventory containers where a PlayerInventory can hold individual loose items (LootItem like IronSword or HealthPotion) as well as container bags (ContainerBag like LeatherBackpack), which in turn hold smaller pouches (HerbPouch, BeltPouch) to arbitrary depth while calculating total weight and gold value.
Writing separate loops and type-checks for individual items versus containers (if (isBag) ... else ...) breaks recursive nesting. Naive designs force separate vectors for items and bags, duplicate traversal logic on every depth level, and fail to treat single items and composite pouches uniformly.
The Composite Pattern unifies leaf items and container groups behind the shared InventoryNode interface, allowing trees of arbitrary depth to be queried through a single recursive call. Next in Sprint 3 (C3 Β· Memento), before entering a boss room with their equipped gear and inventory, the player needs to save a checkpoint and roll back on death without exposing Hero's private fields.
The product ticket
In our RPG engine, heroes collect loot: single items like an IronSword, a HealthPotion, and gemstones. As adventures continue, players organize inventory using containers: a large LeatherBackpack, inside of which they tuck a BeltPouch, an HerbPouch, or a GemChest to arbitrary depth.
- β’
Support both primitive single items (
LootItem) and container bags (ContainerBag). - β’
Allow bags to hold both loose items and other bags to arbitrary nesting depth.
- β’
Calculate
totalWeight()recursively across all contents (including empty container tare weight) to enforce character encumbrance limits. - β’
Calculate
totalGoldValue()across all items inside nested containers for vendor sales and wealth checks. - β’
Provide a hierarchical visualization via
print(int indent)showing the tree structure.
Designers want to introduce new specialized containers: LockedChest, EnchantedQuiver, and AlchemistKit. The inventory calculation logic must remain completely untouched when new container variants are introduced.
Step 1 β The naive code & why it breaks
The naive implementation creates distinct Item and Bag structs, with NaiveInventory holding separate vectors looseItems_ and bags_ where Bag holds std::vector<Item>.
#include <print>
#include <string>
#include <vector>
// Naive design: separate structs for Item and Bag, hardcoded to 1 level of nesting!
struct Item {
std::string name;
double weight;
int goldValue;
};
struct Bag {
std::string name;
double emptyWeight;
std::vector<Item> items; // Only holds flat items, CANNOT hold another Bag!
};
class NaiveInventory {
std::vector<Item> looseItems_;
std::vector<Bag> bags_;
public:
void addLooseItem(Item it) {
looseItems_.push_back(std::move(it));
}
void addBag(Bag b) {
bags_.push_back(std::move(b));
}
double totalWeight() const {
double total = 0.0;
for (const auto& item : looseItems_) {
total += item.weight;
}
for (const auto& bag : bags_) {
total += bag.emptyWeight;
for (const auto& item : bag.items) {
total += item.weight;
}
}
return total;
}
int totalGoldValue() const {
int total = 0;
for (const auto& item : looseItems_) {
total += item.goldValue;
}
for (const auto& bag : bags_) {
for (const auto& item : bag.items) {
total += item.goldValue;
}
}
return total;
}
};
int main() {
NaiveInventory inv;
inv.addLooseItem({"Iron Sword", 5.0, 150});
inv.addLooseItem({"Health Potion", 0.5, 25});
Bag backpack{"Adventurer's Backpack", 1.2, {
{"Rope", 2.0, 10},
{"Torch", 1.0, 5}
}};
inv.addBag(backpack);
// Problem: Player found a "Herb Pouch" (a bag).
// How to put Herb Pouch inside Adventurer's Backpack?
// Naive Bag only holds std::vector<Item>, so it's impossible without hacking the struct!
std::println("[Naive Inventory] Total weight: {:.1f} kg, Total value: {} gold",
inv.totalWeight(), inv.totalGoldValue());
}A Bag can only contain flat Items. If a player places an HerbPouch inside a LeatherBackpack, the naive model cannot represent it without constantly adding nested vector<Bag> subBags fields.
Computing total weight requires handwritten nested loops across loose items, then bags, then items inside bags. Adding a 3rd level of nesting forces rewriting every traversal function.
Callers must distinguish between addItem(item) and addBag(bag). Client code cannot treat a single item and a container uniformly, violating polymorphism.
Step 2 β The fix: Composite Pattern
Compose objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions of objects uniformly.
We define an abstract InventoryNode interface declaring weight(), goldValue(), and print(). Leaf nodes (LootItem) return their own values directly. Composite nodes (ContainerBag) own a collection of children via std::vector<std::unique_ptr<InventoryNode>> and recursively aggregate values from their children. The root PlayerInventory interacts purely with InventoryNode, so a bag containing 50 pouches is evaluated with the exact same call as a single sword.
InventoryNode β declares common operations (name(), weight(), goldValue(), print()) shared by items and containers.
LootItem β primitive items (sword, potion, gem) that have no children and implement base calculations.
ContainerBag β container holding std::vector<std::unique_ptr<InventoryNode>> and recursively delegating operations to its children.
PlayerInventory β manages the root inventory container and provides top-level access.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.