From State tennis to endomorphism by Mark Seemann
You can refactor the State pattern to pure functions.
In a previous article you saw how to do the Tennis kata with the State design pattern. Like most other patterns in Design Patterns, the State pattern relies on mutation. If you favour functional programming and immutable data, you may not like that. Fortunately, converting the API to immutable data and pure functions is plain sailing.
In this post I'll show you how I did it.
Return Score #
Recall from the previous article that the IScore
interface defined a single method, BallTo
:
public interface IScore { void BallTo(Player winner, Game game); }
With its void
return type, it clearly indicate that BallTo
mutates the state of something - although it's less clear whether it's the object itself, game
, or both.
As a first step towards turning the method into a pure function, then, you can change the return type so that it returns an IScore
object:
public interface IScore { IScore BallTo(Player winner, Game game); }
In itself, this doesn't guarantee that the function is pure. In fact, after this small step, none of the implementations are. Here, for example, is the updated Advantage
implementation:
public IScore BallTo(Player winner, Game game) { if (winner == Player) game.Score = new CompletedGame(winner); else game.Score = Deuce.Instance; return game.Score; }
This implementation still modifies game.Score
before returning it. All the other IScore
implementations do the same.
Use the returned score #
Now that the BallTo
method returns an IScore
object, you can edit the Game
class' BallTo
method so that it uses the returned value:
public void BallTo(Player player) { Score = Score.BallTo(player, this); }
Given that all the IScore
implementations currently mutate game.Score
, this seems redundant, but sets you up for the next refactoring step.
Remove State mutation #
You can now remove the mutation of game.Score
from all the implementations of IScore
. Here's Advantage
after the refactoring:
public IScore BallTo(Player winner, Game game) { if (winner == Player) return new CompletedGame(winner); else return Deuce.Instance; }
Notice that this implementation no longer uses the game
parameter.
The other IScore
implementations get a similar treatment.
Remove game parameter #
Since no implementations use the game
parameter you can remove it from the interface:
public interface IScore { IScore BallTo(Player winner); }
and, of course, from each of the implementations:
public IScore BallTo(Player winner) { if (winner == Player) return new CompletedGame(winner); else return Deuce.Instance; }
The above method, again, is the implementation of Advantage
.
Return Game #
You can now make the same sequence of changes to the Game
class itself. Recall from above that its BallTo
method returns void
. As a the first refactoring step towards turning that method into a pure function, then, change the return type:
public Game BallTo(Player player) { Score = Score.BallTo(player); return this; }
The mutation remains a little while longer, but the method looks like something that could be a pure function.
Return new Game #
The next refactoring step is to return a new Game
instance instead of the same (mutated) instance:
public Game BallTo(Player player) { Score = Score.BallTo(player); return new Game(Score); }
The first line still mutates Score
, but now you're only one step away from an immutable implementation.
Remove Game mutation #
Finally, you can remove the mutation of the Game
class. First, remove the internal
setter from the Score
property:
public IScore Score { get; }
You can now lean on the compiler, as Michael Feathers explains in Working Effectively with Legacy Code. This forces you to fix the the BallTo
method:
public Game BallTo(Player player) { return new Game(Score.BallTo(player)); }
This is also the only refactoring that requires you to edit the unit tests. Here a few methods as examples:
[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)); var actual = sut.BallTo(winner); Assert.Equal(new CompletedGame(winner), actual.Score); } [Theory] [InlineData(Player.One)] [InlineData(Player.Two)] public void FortyThirty(Player player) { var sut = new Game(new Forty(player, Point.Thirty)); var actual = sut.BallTo(player.Other()); Assert.Equal(Deuce.Instance, actual.Score); }
These are the same test methods as shown in the previous article. The changes are: the introduction of the actual
variable, and that the assertion now compares the expected value to actual.Score
rather than sut.Score
.
Both variations of BallTo
are now endomorphisms.
Explicit endomorphism #
If you're not convinced that the refactored IScore
interface describes an endomorphism, you can make it explicit - strictly for illustrative purposes. First, introduce an explicit IEndomorphism
interface:
public interface IEndomorphism<T> { T Run(T x); }
This is the same interface as already introduced in the article Builder as a monoid. To be clear, I wouldn't use such an interface in normal C# code. I only use it here to illustrate how the BallTo
method describes an endomorphism.
You can turn a Player
into an endomorphism with an extension method:
public static IEndomorphism<IScore> ToEndomorphism(this Player player) { return new ScoreEndomorphism(player); } private struct ScoreEndomorphism : IEndomorphism<IScore> { public ScoreEndomorphism(Player player) { Player = player; } public Player Player { get; } public IScore Run(IScore score) { return score.BallTo(Player); } }
This is equivalent to partial function application. It applies the player
, and by doing that returns an IEndomorphism<IScore>
.
The Game
class' BallTo
implementation can now Run
the endomorphism:
public Game BallTo(Player player) { IEndomorphism<IScore> endo = player.ToEndomorphism(); IScore newScore = endo.Run(Score); return new Game(newScore); }
Again, I'm not recommending this style of C# programming. I'm only showing this to illustrate how the object playing the State role now describes an endomorphism.
You could subject the Game
class' BallTo
method to the same treatment, but if you did, you'd have to call the extension method something that would distinguish it from the above ToEndomorphism
extension method, since C# doesn't allow overloading exclusively on return type.
Conclusion #
Like many of the other patterns in Design Patterns, the State pattern relies on mutation. It's straightforward, however, to refactor it to a set of pure functions. For what it's worth, these are all endomorphisms.
This article used a take on the tennis kata as an example.