LESSON 0003 · Objects that hold objects · Pharo course

Lesson 0003 — From Kata to App

A Class That Owns a Collection

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.

The win You’ll make an object whose state is a collection — a 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.

1 · The idea

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.

2 · Dictionary — the essentials

A Dictionary maps keys to values. The four messages you’ll use constantly (all verified live):

MessageDoes
d at: #a put: 3store 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 keysall values  ·  all keys (both are collections — iterate them!)
The key trick 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.

3 · Build TallyBoard — test-first (red → green → refactor)

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)

Cycle 1 — a board counts bumps by name

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
Pharo helps you here When you accept (Cmd+S) this test, the compiler flags 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.

Cycle 2 — an unknown name reads as zero

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.

4 · Quick recall — predict, then reveal

a. After b bump: 'x'; bump: 'x'; bump: 'y' on a fresh board, what is b countFor: 'x'?

reveal

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)?

reveal

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?

reveal

counters values

values answers a collection of the Counters; keys answers the names. Both are iterable with do:/select:/inject:into:.

5 · Hands-on — you write the summaries (I’ll check it live)

Task · in your Counter-Esug image · test-first

Keep the loop from section 3: redgreen → 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
reveal

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.

Bonus Add 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.

Primary source

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.