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:
commit
c3355063f2
26 changed files with 4725 additions and 0 deletions
46
shopping_cart/lib/shopping_cart.dart
Normal file
46
shopping_cart/lib/shopping_cart.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// TODO: Implement ShoppingCart using TDD.
|
||||
//
|
||||
// Domain rules (implement one at a time, test first!):
|
||||
// 1. Add an item (name, price, quantity) to the cart
|
||||
// 2. Get the subtotal (sum of price × quantity for all items)
|
||||
// 3. Remove an item by name
|
||||
// 4. Get total item count (sum of all quantities)
|
||||
// 5. Apply a percentage discount to the subtotal (e.g. 10% off)
|
||||
// 6. Items with zero or negative price are rejected
|
||||
//
|
||||
// CartItem is a Value Object — immutable, validated at construction.
|
||||
// ShoppingCart is stateful — it accumulates items across calls.
|
||||
|
||||
class CartItem {
|
||||
final String name;
|
||||
final double price;
|
||||
final int quantity;
|
||||
|
||||
CartItem({required this.name, required this.price, required this.quantity}) {
|
||||
// TODO: validate price > 0 and quantity > 0
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCart {
|
||||
// TODO: implement
|
||||
|
||||
void addItem(CartItem item) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void removeItem(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
double subtotal() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int itemCount() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
double totalAfterDiscount(double discountPercent) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
144
shopping_cart/lib/shopping_cart_solution.dart
Normal file
144
shopping_cart/lib/shopping_cart_solution.dart
Normal 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!
|
||||
Loading…
Add table
Add a link
Reference in a new issue