Initial commit: Complete TDD workshop materials

- Workshop documentation (WORKSHOP_PLAN, FACILITATOR_GUIDE, etc.)
- FizzBuzz kata with demo script (git history to be recreated)
- Password Validator kata with demo script and solution
- Shopping Cart kata with demo script and solution
- Setup guide and TDD reference card for participants
This commit is contained in:
fiatcode 2026-03-10 15:32:21 +07:00
commit c3355063f2
26 changed files with 4725 additions and 0 deletions

View file

@ -0,0 +1,144 @@
// 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<CartItem>
// 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<String, CartItem> _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<CartItem> because that's the obvious choice:
//
// final List<CartItem> _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!