LESSON 0003 · Objects that hold objects · Pharo course
Lesson 0003 — From Kata to App
Every real app is objects holding other objects and answering questions about them. You’ll build a TallyBoard that manages many named counters — the bridge from kata to application.
Dictionary of counters — and give it methods that summarise what it holds (a total, the names above a threshold). This is the exact shape of a shopping cart, a scoreboard, a playlist, an inventory.
Your Counter holds a single number. An application object holds a collection of other objects and coordinates them. The collection lives in an instance variable; the methods read and update it. Nothing new syntactically — you already have classes (0001) and iteration (0002). We just point them at a Dictionary.
A Dictionary maps keys to values. The four messages you’ll use constantly (all verified live):
| Message | Does |
|---|---|
| d at: #a put: 3 | store value 3 under key #a |
| d at: #a ifAbsent: [ 0 ] | read; if key missing, answer the block’s value (no error) |
| d at: #a ifAbsentPut: [ Counter new ] | create-on-demand: if missing, store & return the block’s value |
| d values · d keys | all values · all keys (both are collections — iterate them!) |
at:ifAbsentPut: is how you “get or make” in one line. (counters at: aName ifAbsentPut: [ Counter new ]) returns the counter for a name, creating a fresh one the first time you see that name. No if needed.
Just like your Counter, we grow this from tests. The loop: write a failing test (red), write the least code to pass it (green), then tidy (refactor). We never write a method the tests don’t demand. (Pharo MOOC, TDD with SUnit)
Red. First the test class, then a failing test. Create TallyBoardTest:
TestCase subclass: #TallyBoardTest
instanceVariableNames: ''
classVariableNames: ''
package: 'Counter-Tests'
testBumpingCountsByName
| b |
b := TallyBoard new.
b bump: 'a'; bump: 'a'; bump: 'b'.
self assert: (b countFor: 'a') equals: 2.
self assert: (b countFor: 'b') equals: 1
TallyBoard as undeclared and offers to create the class for you. Accept it, then add the instance variable counters to its definition. Run the test → red: TallyBoard does not understand bump:.
Green. Write the minimum to pass — three methods:
initialize
counters := Dictionary new
bump: aName
(counters at: aName ifAbsentPut: [ Counter new ]) increment
countFor: aName
^ (counters at: aName) count
Run → green. Note bump: uses at:ifAbsentPut: to get-or-create the named counter, then sends it increment. TallyBoard delegates to Counter — it never touches count directly.
Red. A new test forces a design decision — what should an un-bumped name return?
testUnknownNameIsZero
self assert: (TallyBoard new countFor: 'ghost') equals: 0
Run → red, but not a failed assertion — an error: KeyNotFound: key 'ghost' not found. Your countFor: did counters at: aName on a missing key. The test just told you the current design is wrong.
Green. Let the test drive the fix — answer a missing name with a fresh (zero) Counter instead of erroring:
countFor: aName
^ (counters at: aName ifAbsent: [ Counter new ]) count
Run both tests → green. This is why the method uses ifAbsent: — a test demanded it, not a guess. Refactor if anything reads awkwardly; the green tests are your safety net.
a. After b bump: 'x'; bump: 'x'; bump: 'y' on a fresh board, what is b countFor: 'x'?
2
First bump: 'x' creates a Counter for 'x' and increments (1); second increments the same one (2). 'y' is a different key.
b. On that same board, what is b countFor: 'z' (never bumped)?
0
at:ifAbsent: [ Counter new ] returns a brand-new Counter whose count is 0 — and does not store it.
c. Which message gives you every counter object to iterate over?
counters values
values answers a collection of the Counters; keys answers the names. Both are iterable with do:/select:/inject:into:.
Keep the loop from section 3: red → green → refactor. Write the test before the method every time.
1 · total. First a red test in TallyBoardTest — build a board, bump a few names, assert the total. Run it (red — no total yet). Then make it green by folding the values (0002!):
total
^ counters values inject: 0 into: [ :sum :c | sum + c count ]
2 · namesAbove: — write this one entirely yourself, test-first. Add a red test asserting which names have a count above some number, watch it fail, then implement namesAbove: aNumber. Hint: select: over counters keys, testing (counters at: name) count.
Tip: a setUp that builds a shared board (bump a few names different numbers of times) keeps each test short — as your CounterTest already does.
Predict this before running it in a Playground:
| b | b := TallyBoard new.
b bump: 'a'; bump: 'a'; bump: 'b'.
b total
3
'a' counted twice + 'b' once = 3. total folds #(2 1) into 3.
Then say “done” — I’ll read TallyBoard through Iris, run TallyBoardTest, and give feedback.
busiest — the name of the counter with the highest count. Ask me for the inject:into: pattern that folds to “the best so far,” like the max fold you already saw.
This week: Pharo MOOC — the “Dictionary” and “Defining classes” segments. Reference: the new Dictionary section on the Collections & Blocks card. Deeper: Pharo by Example, collections chapter.
Stuck on namesAbove: or the test setup? Ask me — I can eval fragments in your image as you go, or hand you the busiest pattern.