Refactoring the TCP State pattern example to pure functions by Mark Seemann
A C# example.
This article is one of the examples that I promised in the earlier article The State pattern and the State monad. That article examines the relationship between the State design pattern and the State monad. That article is deliberately abstract, so one or more examples are in order.
In this article, I show you how to start with the example from Design Patterns and refactor it to an immutable solution using pure functions.
The code shown here is available on GitHub.
TCP connection #
The example is a class that handles TCP connections. The book's example is in C++, while I'll show my C# interpretation.
A TCP connection can be in one of several states, so the TcpConnection
class keeps an instance of the polymorphic TcpState
, which implements the state and transitions between them.
TcpConnection
plays the role of the State pattern's Context
, and TcpState
of the State
.
public class TcpConnection { public TcpState State { get; internal set; } public TcpConnection() { State = TcpClosed.Instance; } public void ActiveOpen() { State.ActiveOpen(this); } public void PassiveOpen() { State.PassiveOpen(this); } // More members that delegate to State follows...
The TcpConnection
class' methods delegate to a corresponding method on TcpState
, passing itself an argument. This gives the TcpState
implementation an opportunity to change the TcpConnection
's State
property, which has an internal
setter.
State #
This is the TcpState
class:
public class TcpState { public virtual void Transmit(TcpConnection connection, TcpOctetStream stream) { } public virtual void ActiveOpen(TcpConnection connection) { } public virtual void PassiveOpen(TcpConnection connection) { } public virtual void Close(TcpConnection connection) { } public virtual void Synchronize(TcpConnection connection) { } public virtual void Acknowledge(TcpConnection connection) { } public virtual void Send(TcpConnection connection) { } }
I don't consider this entirely idiomatic C# code, but it seems closer to the book's C++ example. (It's been a couple of decades since I wrote C++, so I could be mistaken.) It doesn't matter in practice, but instead of a concrete class with no-op virtual
methods, I would usually define an interface. I'll do that in the next example article.
The methods have the same names as the methods on TcpConnection
, but the signatures are different. All the TcpState
methods take a TcpConnection
parameter, whereas the TcpConnection
methods take no arguments.
While the TcpState
methods don't do anything, various classes can inherit from the class and override some or all of them.
Connection closed #
The book shows implementations of three classes that inherit from TcpState
, starting with TcpClosed
. Here's my translation to C#:
public class TcpClosed : TcpState { public static TcpState Instance = new TcpClosed(); private TcpClosed() { } public override void ActiveOpen(TcpConnection connection) { // Send SYN, receive SYN, Ack, etc. connection.State = TcpEstablished.Instance; } public override void PassiveOpen(TcpConnection connection) { connection.State = TcpListen.Instance; } }
This implementation overrides ActiveOpen
and PassiveOpen
. In both cases, after performing some work, they change connection.State
.
"
TCPState
subclasses maintain no local state, so they can be shared, and only one instance of each is required. The unique instance ofTCPState
subclass is obtained by the staticInstance
operation. [...]"This make each
TCPState
subclass a Singleton [...]."
I've maintained that property of each subclass in my C# code, even though it has no impact on the structure of the State pattern.
The other subclasses #
The next subclass, TcpEstablished
, is cast in the same mould:
public class TcpEstablished : TcpState { public static TcpState Instance = new TcpEstablished(); private TcpEstablished() { } public override void Close(TcpConnection connection) { // send FIN, receive ACK of FIN connection.State = TcpListen.Instance; } public override void Transmit( TcpConnection connection, TcpOctetStream stream) { connection.ProcessOctet(stream); } }
As is TcpListen
:
public class TcpListen : TcpState { public static TcpState Instance = new TcpListen(); private TcpListen() { } public override void Send(TcpConnection connection) { // Send SYN, receive SYN, ACK, etc. connection.State = TcpEstablished.Instance; } }
I admit that I find these examples a bit anaemic, since there's really no logic going on. None of the overrides change state conditionally, which would be possible and make the examples a little more interesting. If you're interested in an example where this happens, see my article Tennis kata using the State pattern.
Refactor to pure functions #
There's only one obvious source of impurity in the example: The literal State
mutation of TcpConnection
:
public TcpState State { get; internal set; }
While client code can't set
the State
property, subclasses can, and they do. After all, it's how the State pattern works.
It's quite a stretch to claim that if we can only get rid of that property setter then all else will be pure. After all, who knows what all those comments actually imply:
// Send SYN, receive SYN, ACK, etc.
To be honest, we must imagine that I/O takes place here. This means that even though it's possible to refactor away from mutating the State
property, these implementations are not really going to be pure functions.
I could try to imagine what that SYN
and ACK
would look like, but it would be unfounded and hypothetical. I'm not going to do that here. Instead, that's the reason I'm going to publish a second article with a more realistic and complex example. When it comes to the present example, I'm going to proceed with the unreasonable assumption that the comments hide no nondeterministic behaviour or side effects.
As outlined in the article that compares the State pattern and the State monad, you can refactor state mutation to a pure function by instead returning the new state. Usually, you'd have to return a tuple, because you'd also need to return the 'original' return value. Here, however, the 'return type' of all methods is void
, so this isn't necessary.
void
is isomorphic to unit, so strictly speaking you could refactor to a return type like Tuple<Unit, TcpConnection>
, but that is isomorphic to TcpConnection
. (If you need to understand why that is, try writing two functions: One that converts a Tuple<Unit, TcpConnection>
to a TcpConnection
, and another that converts a TcpConnection
to a Tuple<Unit, TcpConnection>
.)
There's no reason to make things more complicated than they have to be, so I'm going to use the simplest representation: TcpConnection
. Thus, you can get rid of the State
mutation by instead returning a new TcpConnection
from all methods:
public class TcpConnection { public TcpState State { get; } public TcpConnection() { State = TcpClosed.Instance; } private TcpConnection(TcpState state) { State = state; } public TcpConnection ActiveOpen() { return new TcpConnection(State.ActiveOpen(this)); } public TcpConnection PassiveOpen() { return new TcpConnection(State.PassiveOpen(this)); } // More members that delegate to State follows...
The State
property no longer has a setter; there's only a public getter. In order to 'change' the state, code must return a new TcpConnection
object with the new state. To facilitate that, you'll need to add a constructor overload that takes the new state as an input. Here I made it private
, but making it more accessible is not prohibited.
This implies, however, that the TcpState
methods also return values instead of mutating state. The base class now looks like this:
public class TcpState { public virtual TcpState Transmit(TcpConnection connection, TcpOctetStream stream) { return this; } public virtual TcpState ActiveOpen(TcpConnection connection) { return this; } public virtual TcpState PassiveOpen(TcpConnection connection) { return this; } // And so on...
Again, all the methods previously 'returned' void
, so while, according to the State monad, you should strictly speaking return Tuple<Unit, TcpState>
, this simplifies to TcpState
.
Individual subclasses now do their work and return other TcpState
implementations. I'm not going to tire you with all the example subclasses, so here's just TcpEstablished
:
public class TcpEstablished : TcpState { public static TcpState Instance = new TcpEstablished(); private TcpEstablished() { } public override TcpState Close(TcpConnection connection) { // send FIN, receive ACK of FIN return TcpListen.Instance; } public override TcpState Transmit( TcpConnection connection, TcpOctetStream stream) { TcpConnection newConnection = connection.ProcessOctet(stream); return newConnection.State; } }
The trickiest implementation is Transmit
, since ProcessOctet
returns a TcpConnection
while the Transmit
method has to return a TcpState
. Fortunately, the Transmit
method can achieve that goal by returning newConnection.State
. It feels a bit roundabout, but highlights a point I made in the previous article: The TcpConnection
and TcpState
classes are isomorphic - or, they would be if we made the TcpConnection
constructor overload public. Thus, the TcpConnection
class is redundant and might be deleted.
Conclusion #
This article shows how to refactor the TCP connection sample code from Design Patterns to pure functions.
If it feels as though something's missing there's a good reason for that. The example, as given, is degenerate because all methods 'return' void
, and we don't really know what the actual implementation code (all that Send SYN, receive SYN, ACK, etc.) looks like. This means that we actually don't have to make use of the State monad, because we can get away with endomorphisms. All methods on TcpConnection
are really functions that take TcpConnection
as input (the instance itself) and return TcpConnection
. If you want to see a more realistic example showcasing that perspective, see my article From State tennis to endomorphism.
Even though the example is degenerate, I wanted to show it because otherwise you might wonder how the book's example code fares when exposed to the State monad. To be clear, because of the nature of the example, the State monad never becomes necessary. Thus, we need a second example.
Next: Refactoring a saga from the State pattern to the State monad.