Tennis kata using the State pattern by Mark Seemann
An example of using the State design pattern.
Regular readers of this blog will know that I keep coming back to the tennis kata. It's an interesting little problem to attack from various angles.
I don't think you have to do the kata that many times before you realise that you're describing a simple state machine. A few years ago I decided to use that insight to get reacquainted with the State design pattern.
In this article I'll show you what the code looks like.
Context #
As part of the exercise, I decided to stay close to the pattern description in Design Patterns. The public API should be exposed as a single class that hides all the internal state machinery. In the general pattern description, this class is called Context
. The TCP example given in the book, however, calls the example class TCPConnection
. This indicates that you don't have to use the word context when naming the class. I chose to simply call it Game
:
public class Game { public Game() : this(new Points(Point.Love, Point.Love)) { } public Game(IScore score) { Score = score; } public IScore Score { get; internal set; } public void BallTo(Player player) { Score.BallTo(player, this); } }
Since the Game
class delegates all behaviour to its Score
property, it's basically redundant. This may be a degenerate example, but as an exercise of staying true to the pattern, I decided to keep it. It's the class that all tests work through.
Test #
All tests look similar. This parametrised test verifies what happens after deuce:
[Theory] [InlineData(Player.One)] [InlineData(Player.Two)] public void ScoreDeuce(Player winner) { var sut = new Game(Deuce.Instance); sut.BallTo(winner); Assert.Equal(new Advantage(winner), sut.Score); }
This is code that I wrote years ago, so it uses xUnit.net 2.3.1 and runs on .NET Framework 4.6.1, but I don't think it'd have looked different today. It follows my heuristic for formatting unit tests.
Structural equality #
The equality assertion works because Advantage
has structural equality. In this exercise, I found it simpler to declare types as value types instead of overriding Equals
and GetHashCode
:
public struct Advantage : IScore { public Advantage(Player player) { Player = player; } public Player Player { get; set; } public void BallTo(Player winner, Game game) { if (winner == Player) game.Score = new CompletedGame(winner); else game.Score = Deuce.Instance; } }
This turned out to be possible throughout, since all types emerged as mere compositions of other value types. The above Advantage
struct, for example, adapts a Player
, which, unsurprisingly, is an enum
:
public enum Player { One = 0, Two }
One of the states holds no data at all, so I made it a Singleton, as suggested in the book. (Contrary to popular belief, I don't consider Singleton an anti-pattern.)
public struct Deuce : IScore { public readonly static IScore Instance = new Deuce(); public void BallTo(Player winner, Game game) { game.Score = new Advantage(winner); } }
Since it's a Singleton, from an equality perspective it doesn't matter whether it's a value or reference type, but I made it a struct
for consistency's sake.
State #
In the State design pattern's formal structure, the Context
delegates all behaviour to an abstract State
class. Since I consider inheritance harmful (as well as redundant), I instead chose to model the state as an interface:
public interface IScore { void BallTo(Player winner, Game game); }
As the pattern suggests, the State object exposes methods that take the Context as an extra parameter. This enables concrete State implementation to change the state of the Context, as both the above structs (Advantage
and Deuce
) demonstrate. They both implement the interface.
When I do the kata, I always seem to arrive at five distinct states. I'm not sure if this reflects the underlying properties of the problem, or if it's just because that's what worked for me years ago, and I'm now stuck in some cognitive local maximum. In any case, that's what happened here as well. Apart from the above Advantage
and Deuce
there's also a Forty
implementation:
public struct Forty : IScore { public Forty(Player player, Point otherPlayerPoint) { Player = player; OtherPlayerPoint = otherPlayerPoint; } public Player Player { get; } public Point OtherPlayerPoint { get; } public void BallTo(Player winner, Game game) { if (Player == winner) game.Score = new CompletedGame(winner); else if (OtherPlayerPoint == Point.Thirty) game.Score = Deuce.Instance; else if (OtherPlayerPoint == Point.Fifteen) game.Score = new Forty(Player, Point.Thirty); else game.Score = new Forty(Player, Point.Fifteen); } }
Another thing that I've also noticed when doing the Tennis kata is that the state logic for advantage and deuce is simple, whereas the state transitions involving points is more complicated. If you think Forty
looks complicated, then consider Points
:
public struct Points : IScore { public Points(Point playerOnePoint, Point playerTwoPoint) { PlayerOnePoint = playerOnePoint; PlayerTwoPoint = playerTwoPoint; } public Point PlayerOnePoint { get; } public Point PlayerTwoPoint { get; } public void BallTo(Player winner, Game game) { var pp = PlayerPoint(winner); var opp = PlayerPoint(winner.Other()); if (pp == Point.Thirty) game.Score = new Forty(winner, opp); else if (winner == Player.One) game.Score = new Points(Increment(PlayerOnePoint), PlayerTwoPoint); else game.Score = new Points(PlayerOnePoint, Increment(PlayerTwoPoint)); } private Point PlayerPoint(Player player) { if (player == Player.One) return PlayerOnePoint; else return PlayerTwoPoint; } private static Point Increment(Point point) { if (point == Point.Love) return Point.Fifteen; else return Point.Thirty; } }
The last IScore
implementation represents a completed game:
public struct CompletedGame : IScore { public CompletedGame(Player player) { Player = player; } public Player Player { get; } public void BallTo(Player winner, Game game) { } }
In a completed game, the BallTo
implementation is a no-op, because Player
has already won the game.
Miscellany #
Here's a few more tests, just to back up my claim that all tests look similar:
[Theory] [InlineData(Player.One, Point.Love)] [InlineData(Player.One, Point.Fifteen)] [InlineData(Player.One, Point.Thirty)] [InlineData(Player.Two, Point.Love)] [InlineData(Player.Two, Point.Fifteen)] [InlineData(Player.Two, Point.Thirty)] public void FortyWins(Player winner, Point otherPlayerPoint) { var sut = new Game(new Forty(winner, otherPlayerPoint)); sut.BallTo(winner); Assert.Equal(new CompletedGame(winner), sut.Score); } [Theory] [InlineData(Player.One)] [InlineData(Player.Two)] public void FortyThirty(Player player) { var sut = new Game(new Forty(player, Point.Thirty)); sut.BallTo(player.Other()); Assert.Equal(Deuce.Instance, sut.Score); }
The second of these test methods uses an extension method called Other
:
public static class PlayerEnvy { public static Player Other(this Player player) { if (player == Player.One) return Player.Two; else return Player.One; } }
As is my custom, I named the class containing the extension method with the Envy
suffix, because I often consider this kind of extension method a sign of Feature Envy. Alas, in C# you can't add methods to an enum
.
Conclusion #
Implementing the tennis kata with the classic State pattern is straightforward.
After having spent the majority of the last decade with functional programming, I've come to realise that many problems are really just state machines waiting to be revealed as such. Implementing a finite state machine in a language with algebraic data types is so easy that you often reach for that kind of modelling.
Before I learned functional programming, when all I knew was procedural and object-oriented code, I rarely thought of problems in terms of finite state machines. Now I see them everywhere. It's an example of how learning a completely different thing can feed back on everyday programming.
Once you recognise that a problem can be modelled as a finite state machine, you have new options. If you're in a conservative context where colleagues aren't keen on fancy FP shenanigans, you can always reach for the State design pattern.
Comments
Do you think that perhaps you are at risk of making too many problems look like nails for your state machine hammer? :-) Actually, you just want to convert a pair of points into a tennis score. That doesn't require a state machine, I don't think:
Jim, thank you for writing. You're right: a state machine isn't required. It's a nice judo trick to keep track of the server and receiver points as two different numbers. That does simplify the code.
I tried something similar many years ago (after all, the kata description itself strongly hints at that alternative perspective), but for various reasons ended with an implementation that wasn't as nice as yours. I never published it. I've done this exercise many times, and I've only published the ones that I find can be used to highlight some interesting point.
The point of doing a coding kata is to experiment with variation. The goal isn't always to reach the fewest lines of code, or complete the exercise as fast as possible. These can be interesting exercises in their own rights, but by doing a kata with other constraints can be illuminating as well.
My goal with this variation was mainly to get reacquainted with the State pattern. Actually 'solving' the problem is a requirement, but not the goal.
Modelling the problem with the State pattern has advantages and disadvantages. A major advantage is that it offers an API that enables client code to programmatically distinguish between the various states. When I did the exercise similar to your code, asserting against a string is easy. However, basing an API on a returned string may not be an adequate design. It's okay for an exercise, but imagine that you were asked to translate the scores. For example, in Danish, advantage is instead called fordel. Another requirement might be that you report players by name. So, for example, a Danish score might instead require something like fordel Serena Williams.
Don't take this as a criticism of your code. Sometimes, you don't need more than what you've done, and in such cases, doing more would be over-engineering.
On the other hand, if you find yourself in situations where e.g. translation is required, it can be helpful to be aware that other ways to model a problem are available. That's the underlying benefit of doing katas. The more variations you do, the better prepared you become to 'choose the right tool for the job.'
All that said, though, with the tennis kata, you can make it trivially simple modelling it as a finite state automaton.