Continuing Our Tic-Tac-Toe Design
Thomas J. Kennedy
During the First Design Discussion we covered a number of topics:
- C++, Java, and Python Class Checklists
- Starting a Domain Model
- Building a Domain Model
- Capturing Behaviors
- Building UML Class Diagrams
1 Our Model Thus Far
We concluded the discussion with a Pseudo-Final Domain Model.
@startuml
hide empty members
class Player {
name
symbol
nextMove(theBoard : Board)
}
class Board {
getCell(position)
setCell(position, symbol)
isFull()
}
class Strategy {
}
class Referee {
validate(selectedMove, theBoard : Board)
checkForWin(theBoard: Board)
checkForLoss(theBoard: Board)
checkForStalemate(theBoard: Board)
}
class Game {
player1: Player
player2: Player
getWinner() : Player
getLoser() : Player
playOneRound()
isOver()
endedWithWin()
endedWithLoss()
endedWithStalemate()
}
Player <|---- HumanPlayer
Player <|---- ComputerPlayer
@enduml
2 Validating Our Model
This is the fun part! We will be using two tools:
Now on to the fun part! For real this time…
I will assume you have watched Whirlwind Introduction to UML Sequence Diagrams.
3 Our First Use Case
Our goal is to validate our model. This involves:
- confirming our model can capture the entirety of each case.
- addressing missing elements (both responsibilities and classes).
3.1 Play One Round
1: A new
game
starts with twoplayer
s, Tom and Jay.2: The
referee
flips a coin. Tom and Jay choose heads and tails, respectively.3: The coin lands on tails. Jay will go first as player 1 (‘X’). Tom will second as player 2 (‘O’).
4: Jay selects cell 5 (the center of the board). His move is recorded.
5: Tom selects cell 1 (top-left). His move is recorded.
6: Jay selects cell 3 (top-right). His move is recorded.
7: Tom selects cell 7 (bottom-left). His move is recorded.
8: Jay selects cell 4 (middle-left). His move is recorded.
9: Tom selects cell 9 (bottom-right). His move is recorded.
10: Jay selects cell 8 (middle-right). His move is recorded.
11: The referee recognizes 3 ‘X’ symbols in a row. Jay is declared the winner.
12: Jay’s win is recorded in the Tournament Scoreboard. Jay is awarded 1 point.
13: The scoreboard is updated to indicate that Tom and Jay played 1 match.
Remember, we now have a UML Sequence Diagram in which we work with objects. Let us take this step-by-step.
1: A new
game
starts with twoplayer
s, Tom and Jay.
We have four objects (instances of classes).
#![Play One Round]
# Objects
tom:Player
jay:Player
game:Game[a]
/board:Board[a]
#
game:board.new()
Notice how jay and tom are named objects? I have two players. If I am given names for my objects, I must use those names. Take note of the syntax:
tom:Player
jay:Player
The name goes before the colon. The class goes after the colon. This can be though of as:
Player tom;
Player jay;
I left game and board as anonymous objects. While I have given them names out of necessity, the names are placeholders. They have no inherent meaning. Both game
and board
are anonymous objects.
So far we have been lucky. Our existing model captures the entirety of step 1.
3.2 Step 2: Our First Update
Our luck does not hold in step 2.
2: The
referee
flips a coin. Tom and Jay choose heads and tails, respectively.
We can add the referee object, but the referee class does not have a flip coin behavior. Our Player class does not have a choose heads or tails responsibility. The solution is simple, let us pretend…
#![Play One Round]
# Objects
tom:Player
jay:Player
game:Game[a]
/board:Board[a]
/ref:Referee[a]
#
game:board.new()
game:ref.new()
game:ref.flipCoin()
ref:tomChoice=tom.choose()
ref:jayChoice=jay.choose()
We have identified oversights in our Player (choose), Referee (flipCoin) and Game classes. Yes, Game
needs to be updated. What is missing? How about the function that runs the game from start to finish. I think I will call it playMatch.
Now we can update our UML class diagrams.
@startuml
hide empty members
class Player {
name
symbol
nextMove(theBoard : Board)
choose()
}
class Board {
getCell(position)
setCell(position, symbol)
isFull()
}
class Strategy {
}
class Referee {
validate(selectedMove, theBoard : Board)
checkForWin(theBoard: Board)
checkForLoss(theBoard: Board)
checkForStalemate(theBoard: Board)
flipCoin()
}
class Game {
player1: Player
player2: Player
getWinner() : Player
getLoser() : Player
playMatch()
playOneRound()
isOver()
endedWithWin()
endedWithLoss()
endedWithStalemate()
}
Player <|---- HumanPlayer
Player <|---- ComputerPlayer
@enduml
3.3 Step 2 Cleanup
Things are getting confusing. Let us ignore the HumanPlayer and ComputerPlayer classes for now.
Let us rearrange our UML Sequence Diagram.
#![Play One Round]
# Objects
game:Game[a]
/board:Board[a]
/ref:Referee[a]
tom:Player
jay:Player
#
game:board.new()
game:ref.new()
game:ref.flipCoin()
ref:tomChoice=tom.choose()
ref:jayChoice=jay.choose()
That is much better. We see that Game::playMatch() is driving everything. Let us put :Game in the left-most position. The order of our function calls has not changed. We rearranged classes objects for clarity.
3.4 Step 3
Step 3 is far less complex.
3: The coin lands on tails. Jay will go first as player 1 (‘X’). Tom will second as player 2 (‘O’).
I think we might need setPlayer1 and setPlayer2 methods. These are not the standard C++ (or Java or Python3) setters. These two functions arise from our problem domain!
Let us update our sequence diagram…
#![Play One Round]
# Objects
game:Game[a]
/board:Board[a]
/ref:Referee[a]
tom:Player
jay:Player
#
game:board.new()
game:ref.new()
game:ref.flipCoin()
ref:tomChoice=tom.choose()
ref:jayChoice=jay.choose()
game:setPlayer1(jay)?
game:jay.setSymbol('X')
game:setPlayer2(tom)?
game:tom.setSymbol('O')
Note the question marks next to setPlayer1 and setPlayer2 I am not entirely sure these are needed. So far they appear to be sufficiently captured by the use of setSymbol(…). I will add them to my model… for now.
Let us update our model accordingly.
3.5 Steps 4 to 10
The next 7 (10 - 4 + 1 = 7) steps are the same. Our PLayer::nextMove
and Board::setCell
functions capture the two required steps:
- Get the move from the player.
- Update the board.
Let us capture step 4.
4: Jay selects cell 5 (the center of the board). His move is recorded.
We will add two new lines to our markup:
game:jay.nextMove(...)
game:board.setCell(5, jay.getSymbol())
This corresponds to two function calls. Think about the context of these calls… These calls happen within the Game::playMatch
method (responsibility/behavior).
The next few steps (i.e., steps 5-10) are similar. Let us add them all in one fell swoop.
#![Play One Round]
# Objects
game:Game[a]
/board:Board[a]
/ref:Referee[a]
tom:Player
jay:Player
#
game:board.new()
game:ref.new()
game:ref.flipCoin()
ref:tomChoice=tom.choose()
ref:jayChoice=jay.choose()
game:setPlayer1(jay)?
game:jay.setSymbol('X')
game:setPlayer2(tom)?
game:tom.setSymbol('O')
game:jay.nextMove(...)
game:board.setCell(5, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(1, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(3, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(7, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(4, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(9, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(8, jay.getSymbol())
3.6 Step 11
Step 11 is captured by our existing model.
11: The referee recognizes 3 ‘X’ symbols in a row. Jay is declared the winner.
We can use one line!
#
game:winner=ref.checkforWin(...)
However, I think we should capture the horizontal win more explicitly. Let us add Referee::recognizeHorizontalWin
to our sequence diagram.
#
game:winner=ref.checkforWin(...)
ref:ref.recognizeHorizontalWin(...)
That is much better. I think we should also add Referee::recognizeVerticalWin
and Referee::recognizeDiagonalWin
. We will probably need them in other scenarios.
Let us update our model.
3.7 Steps 12 & 13
I think we can capture the last two steps simultaneously.
12: Jay’s win is recorded in the Tournament Scoreboard. Jay is awarded 1 point.
13: The scoreboard is updated to indicate that Tom and Jay played 1 match.
It looks like we need a new class to capture the Tournament Scoreboard. I think Scoreboard
is a reasonable name. I think steps 12 and 13 lead to Scoreboard::awardPoint(p: Player)
and Scoreboard::recordMatch(p1: Player, p2: Player 2)
, respectively.
That leaves us with our final UML Sequence Diagram.
#![Play One Round]
# Objects
game:Game[a]
/board:Board[a]
/ref:Referee[a]
tom:Player
jay:Player
scoreBoard:ScoreBoard[a]
#
game:board.new()
game:ref.new()
game:ref.flipCoin()
ref:tomChoice=tom.choose()
ref:jayChoice=jay.choose()
game:setPlayer1(jay)?
game:jay.setSymbol('X')
game:setPlayer2(tom)?
game:tom.setSymbol('O')
game:jay.nextMove(...)
game:board.setCell(5, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(1, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(3, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(7, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(4, jay.getSymbol())
game:tom.nextMove(...)
game:board.setCell(9, tom.getSymbol())
game:jay.nextMove(...)
game:board.setCell(8, jay.getSymbol())
#
game:winner=ref.checkforWin(...)
ref:ref.recognizeHorizontalWin(...)
#
game:scoreBoard.awardPoint(jay)
game:scoreBoard.recordMatch(jay, tom)
4 Post First Use Case Domain Model
We are left with a more complete model.
@startuml
hide empty members
class Player {
name
symbol
nextMove(theBoard : Board)
choose()
}
class Board {
getCell(position)
setCell(position, symbol)
isFull()
}
class Strategy {
}
class Referee {
validate(selectedMove, theBoard : Board)
checkForWin(theBoard: Board)
checkForLoss(theBoard: Board)
checkForStalemate(theBoard: Board)
flipCoin()
recognizeHorizontalWin(...)
recognizeVerticalWin(...)
recognizeDiagonalWin(...)
}
class Game {
player1: Player
player2: Player
getWinner() : Player
getLoser() : Player
playMatch()
playOneRound()
isOver()
endedWithWin()
endedWithLoss()
endedWithStalemate()
setPlayer1(p: Player)?
setPlayer2(p: Player)?
}
class Scoreboard {
awardPoint(p: Player)
recordMatch(p1: Player, p2: Player 2)
}
Player <|---- HumanPlayer
Player <|---- ComputerPlayer
@enduml
5 Lingering (And New) Questions
Of course, we can see a number of questions remain. Examine the sequence diagram. Take note of the ellipses (…) in some of the function calls. We need to figure out the required arguments. We also need to determine whether setPlayer1
and setPlayer2
are necessary.
We will leave these questions for later consideration.