// REFERENCE SOLUTION — For facilitator use only. // Do not share with participants before the session. // // This solution shows the evolution of design through TDD: // - Steps 1-2: Basic implementation with List // - Step 3: Refactored to Map when removeItem() test drove the change // - Step 5: Value Object pattern for CartItem validation // STEP 5: Value Object pattern emerged from test "rejects item with zero price" // See: test/shopping_cart_test.dart:139 // // WHY VALIDATE IN CONSTRUCTOR? // - CartItem can't exist in an invalid state // - No need to check validity every time we use it // - Validation happens once, at creation time // - This is the Value Object pattern from Domain-Driven Design class CartItem { final String name; final double price; final int quantity; CartItem({required this.name, required this.price, required this.quantity}) { // STEP 5: From test "rejects item with zero/negative price" // See: test/shopping_cart_test.dart:139-152 // Validation belongs HERE, not in ShoppingCart.addItem() // If validation was in addItem(), what happens when CartItem is created elsewhere? if (price <= 0) { throw ArgumentError('Price must be greater than zero, got: $price'); } // STEP 5: From test "rejects item with zero quantity" // See: test/shopping_cart_test.dart:155-161 if (quantity <= 0) { throw ArgumentError('Quantity must be greater than zero, got: $quantity'); } } } class ShoppingCart { // STEP 3 REFACTOR: Originally a List // Changed to Map when removeItem(name) test made List lookup inefficient // // WHY MAP instead of LIST? // - removeItem() needs to find item by name // - With List: O(n) lookup with .removeWhere() // - With Map: O(1) lookup by key // - The test for "remove by name" drove this design decision // // From test "removing an item reduces the subtotal" // See: test/shopping_cart_test.dart:71 final Map _items = {}; // STEP 2: From test "subtotal reflects a single item added" // See: test/shopping_cart_test.dart:39 // Simple add - using name as key means duplicate names replace (design choice) void addItem(CartItem item) { _items[item.name] = item; // DESIGN CHOICE: This replaces items with duplicate names // Could alternatively accumulate: _items[item.name].quantity += item.quantity // Both are valid! TDD forces us to decide and document via tests // See bonus test: test/shopping_cart_test.dart:168 } // STEP 3: From test "removing an item reduces the subtotal" // See: test/shopping_cart_test.dart:71 // Map.remove() is simple and efficient - this test drove the Map refactoring void removeItem(String name) { _items.remove(name); // Note: remove() on non-existent key does nothing (which is what we want) // Test "removing non-existent item does nothing" confirmed this behavior // See: test/shopping_cart_test.dart:87 } // STEP 2: From test "subtotal reflects a single item added" // See: test/shopping_cart_test.dart:39 // Classic fold operation: accumulate (price × quantity) for each item double subtotal() { return _items.values .fold(0.0, (sum, item) => sum + (item.price * item.quantity)); } // STEP 2: From test "item count reflects quantity of single item" // See: test/shopping_cart_test.dart:45 // Similar to subtotal, but sum quantities instead of prices int itemCount() { return _items.values.fold(0, (sum, item) => sum + item.quantity); } // STEP 4: From test "10% discount reduces subtotal correctly" // See: test/shopping_cart_test.dart:109 // Straightforward percentage calculation // discountPercent is 0-100, so divide by 100 to get decimal double totalAfterDiscount(double discountPercent) { final discount = discountPercent / 100; return subtotal() * (1 - discount); } } // EVOLUTION NOTES: // // Initial Implementation (Steps 1-2): // Started with List because that's the obvious choice: // // final List _items = []; // // void addItem(CartItem item) { // _items.add(item); // } // // This worked for add, subtotal, and itemCount tests. // // REFACTORING TRIGGER (Step 3): // The removeItem(String name) test exposed inefficiency: // // void removeItem(String name) { // _items.removeWhere((item) => item.name == name); // O(n) search! // } // // This works, but felt wrong. The test made us ask: // "Why are we searching by name every time?" // // Refactoring to Map made the code simpler AND more efficient: // _items.remove(name); // O(1) lookup! // // VALUE OBJECT INSIGHT (Step 5): // Initially, participants might put validation in addItem(): // // void addItem(CartItem item) { // if (item.price <= 0) throw ArgumentError('Invalid price'); // _items[item.name] = item; // } // // But this has a problem: what if CartItem is created elsewhere? // Moving validation to CartItem's constructor makes invalid state impossible. // // This is the Value Object pattern: // - Immutable data // - Validation at construction // - Can't exist in invalid state // - Can be passed around safely without rechecking validity // // All these design insights were DRIVEN BY TESTS, not planned upfront!