Tennis Kata with immutable types and a cyclomatic complexity of 1 by Mark Seemann
Recently I had the inclination to do the Tennis Kata a couple of times. The first time I saw it I thought it wasn't terribly interesting as an exercise in C# development. It would basically just be an application of the State pattern, so I decided to make it a bit more interesting. More or less by intuition I decided to give myself the following constraints:
- All types must be immutable
- All members must have a cyclomatic complexity of 1
Now that's more interesting :)
Given these constraints, what would be the correct approach? Given that this is a finite state machine with a fixed number of states, the Visitor pattern will be a good match.
Each player's score can be modeled as a Value Object that can be one of these types:
- ZeroPoints
- FifteenPoints
- ThirtyPoints
- FortyPoints
- AdvantagePoint
- GamePoint
All of these classes implement the IPoints interface:
public interface IPoints { IPoints Accept(IPoints visitor); IPoints LoseBall(); IPoints WinBall(IPoints opponentPoints); IPoints WinBall(AdvantagePoint opponentPoints); IPoints WinBall(FortyPoints opponentPoints); }
The interesting insight here is that until the opponent's score reaches FortyPoints nothing special happens. Those states can be effectively collapsed into the WinBall(IPoints) method. However, when the opponent either has FortyPoints or AdvantagePoint, special things happen, so IPoints has specialized methods for those cases. All implementations should use double dispatch to invoke the correct overload of WinBall, so the Accept method must be implemented like this:
public IPoints Accept(IPoints visitor) { return visitor.WinBall(this); }
That's the core of the Visitor pattern in action. When the implementer of the Accept method is either FortyPoints or AdvantagePoint, the specialized overload will be invoked.
It's now possible to create a context around a pair of IPoints (called a Game) to implement a method to register that Player 1 won a ball:
public Game PlayerOneWinsBall() { var newPlayerOnePoints = this.PlayerTwoScore .Accept(this.PlayerOneScore); var newPlayerTwoPoints = this.PlayerTwoScore.LoseBall(); return new Game( newPlayerOnePoints, newPlayerTwoPoints); }
A similar method for player two simply reverses the roles. (I'm currently reading Lean Architecture, but have yet to reach the chapter on DCI. However, considering what I've already read about DCI, this seems to fit the bill pretty well… although I might be wrong on that account.)
The context calculates new scores for both players and returns the result as a new instance of the Game class. This keeps the Game and IPoints implementations immutable.
The new score for the winner depends on the opponent's score, so the appropriate overload of WinBall should be invoked. The Visitor implementation makes it possible to pick the right overload without resorting to casts and if statements. As an example, the FortyPoints class implements the three WinBall overloads like this:
public IPoints WinBall(IPoints opponentPoints) { return new GamePoint(); } public IPoints WinBall(FortyPoints opponentPoints) { return new AdvantagePoint(); } public IPoints WinBall(AdvantagePoint opponentPoints) { return this; }
It's also important to correctly implement the LoseBall method. In most cases, losing a ball doesn't change the current state of the loser, in which case the implementation looks like this:
public IPoints LoseBall() { return this; }
However, when the player has advantage and loses the ball, he or she loses the advantage, so for the AdvantagePoint class the implementation looks like this:
public IPoints LoseBall() { return new FortyPoints(); }
To keep things simple I decided to implicitly model deuce as both players having FortyPoints, so there's not explicit Deuce class. Thus, AdvantagePoint returns FortyPoints when losing the ball.
Using the Visitor pattern it's possible to keep the cyclomatic complexity at 1. The code has no branches or loops. It's immutable to boot, so a game might look like this:
[Fact] public void PlayerOneWinsAfterHardFight() { var game = new Game() .PlayerOneWinsBall() .PlayerOneWinsBall() .PlayerOneWinsBall() .PlayerTwoWinsBall() .PlayerTwoWinsBall() .PlayerTwoWinsBall() .PlayerTwoWinsBall() .PlayerOneWinsBall() .PlayerOneWinsBall() .PlayerOneWinsBall(); Assert.Equal(new GamePoint(), game.PlayerOneScore); Assert.Equal(new FortyPoints(), game.PlayerTwoScore); }
In case you'd like to take a closer look at the code I'm attaching it to this post. It was driven completely by using the AutoFixture.Xunit extension, so if you are interested in idiomatic AutoFixture code it's also a good example of that.
TennisKata.zip (3.09 MB)
Comments