The Smalltalk-Way To Do A Case Statement
Introduction
While working on the previous section of code a very useful design concept may have gone unnoticed. If you've been creating Object Oriented designs for a while you may already be doing this sort of thing. But since this is a tutorial, I want to go back and review why we did something the way we did.
Traditional Approach
There was a place in the code where we had to be able to decide which direction symbol to use for an adjacent cell. In a traditional programming style we could have expressed this as:
Cell>>adjacentInversionSymbolToUseFor: ourDirectionSymbol
ourDirectionSymbol = #east ifTrue: [^#west].
ourDirectionSymbol = #north ifTrue: [^#south].
ourDirectionSymbol = #west ifTrue: [^#east].
ourDirectionSymbol = #south ifTrue: [^#north].
You may recognize this code as a "switch" or "case" statement. I've even seen a Smalltalk developer once "enhance" his development environment by providing a "case" statement.
Applying A Class Hierarchy
There's another approach. We created a hierarchy of classes that represented the four directions. These classes will continue to be used and extended, based upon what we have done so far. The hierarchy was written to look like this:
Object
GridDirection
GridDirectionEast
GridDirectionNorth
GridDirectionSouth
GridDirectionWest
When we used this hierarchy we did something like this:
Cell>>adjacentInversionSymbolToUseFor: ourDirectionSymbol
| direction |
direction := GridDirection directionFor: ourDirectionSymbol.
^direction adjacentInversionSymbol
We could have even written something more direct like:
Cell>>adjacentInversionSymbolToUseFor: ourDirectionSymbol
^GridDirection adjacentInversionSymbol: ourDirectionSymbol
How Does It Work?
So how did that work? We have a common abstract class to handle the queries that would select one of the directions for us. The first thing we did was assign a class method to each of the direction subclasses that answered their unique direction symbol.
GridDirectionEast class>>directionSymbol
^#east
GridDirectionNorth class>>directionSymbol
^#north
GridDirectionSouth class>>directionSymbol
^#south
GridDirectionWest class>>directionSymbol
^#west
Then we added a class method that made it easy to find the correct direction class.
GridDirection class>>directionFor: aSymbol
^self subclasses detect: [:cls | cls directionSymbol = aSymbol]
Now if each of the subclasses responds to the polymorphic request #adjacentInversionSymbol...
GridDirectionEast class>>adjacentInversionSymbol
^#west
GridDirectionNorth class>>adjacentInversionSymbol
^#south
GridDirectionSouth class>>adjacentInversionSymbol
^#north
GridDirectionWest class>>adjacentInversionSymbol
^#east
We could have added a single class method on GridDirection to do the direct operation.
GridDirection class>>adjacentInversionSymbol: aSymbol
^(self directionFor: aSymbol) adjacentInversionSymbol
Handling Default And Unmatched
What about the default or unmatched case that you often see in a switch statement? The unmatched case could be handled by GridDirection. Now we enhance the #directionFor: code.
GridDirection class>>directionFor: aSymbol
^self subclasses detect: [:cls | cls directionSymbol = aSymbol]
ifNone: [self]
This way if no one has a matching direction symbol (maybe we were sent #foo as a direction to check) the GridDirection itself will get the request. Then we write whatever default behavior we want.
GridDirection class>>adjacentInversionSymbol
^#noneYouIdiot
And for the default case? That's easy too. Again you could just let GridDirection answer for anyone if they didn't have an answer of their own. For example, maybe only north, south should have their own unique answers. Everyone else maybe should default to #east.
Delete the #adjacentInversionSymbol class methods from GridDirectionEast and GridDirectionWest. Then provide a default answer on GridDirection.
GridDirection class>>adjacentInversionSymbol
^#north
If you want both unmatched and defaults, add a class to handle one of them.
Object
GridDirection
GridDirectionEast
GridDirectionNorth
GridDirectionSouth
GridDirectionWest
GridDirectionUnmatched
Change the #directionFor: code again.
GridDirection class>>directionFor: aSymbol
^self subclasses detect: [:cls | cls directionSymbol = aSymbol]
ifNone: [GridDirectionUnmatched]
Now you can implement special behavior for unmatched answers and/or let them fall back up to GridDirection for a default if you want.
Back To The Tutorial
Okay, I just wanted to take a break here and share a bit about how we used classes to handle (and avoid) a bunch of ifTrue/ifFalse situations. Back to our project...