Design Discussion Notes: Part 2

Thomas J. Kennedy

Contents:

During the First Design Discussion we covered a number of topics:

1 Our Model Thus Far

We concluded the discussion with a Pseudo-Final Domain Model.

A Decent Start

../designDiscussion/tic-tac-toe-17.puml
@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:

3.1 Play One Round

1: A new game starts with two players, 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 two players, Tom and Jay.

We have four objects (instances of classes).

sequence1

 
#![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…

sequence2

seq2.sd
#![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.

uml-class-2-01

design2-class-01.puml
@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.

uml-class-2-02

Let us rearrange our UML Sequence Diagram.

sequence3

seq3.sd
#![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…

sequence4

seq4.sd
#![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.

uml-class-2-02

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:

  1. Get the move from the player.
  2. 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.

sequence5

seq5.sd
#![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.

uml-class-2-04

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.

sequence5

seq6.sd
#![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.

uml-class-2-05

design2-class-05.puml
@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.