Section 2

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

Index Page Next Page

Copyright © 2007, 2008, 2009, 2010 Stephan B Wessels    stevewessels@me.com