Earlier this week, I released StatsDGateway, which is a set of ColdFusion components that facilitate the sending of messages from ColdFusion to a statsD server. When I first wrote the library, it was a single component. However, after watching Corey Haines presentation on Simple Design, I went back and refactored the single component into multiple components that each have a much more focused responsibility. The beauty of this is that I can now easily swap in-and-out different implementations to achieve different behaviors. And, this is exactly what I've done with the transport implementations.
When you construct a StatsDClient.cfc instance manually (ie, not using the factory in the StatsDGateway.cfc), you have to use constructor-injection to provide the client with a transportation implementation that adheres to the following interface:
- sendMessage( message )
The actual mechanism of the message sending is totally hidden and left up to the given transport implementation. As such, I can easily change the type of transportation to use UDP or HTTP or even to prevent message-sending altogether and just keep the messages in an internal queue - CaptureTransport.cfc (which is great for unit testing).
The trasport components end up being composable as well. So, I can wrap one transport, such as UDPTransport.cfc, inside of another transport like BufferedTransport.cfc, that will buffer messages until they reach a certain size and then send them all as one concatenated string.
<cfscript> // Create a statsD client that uses a transient UDP socket (one that is created and // closed for each message, which prevents the need for explicit clean-up). client = new lib.client.StatsDClient( new lib.transport.UDPTransport(), new lib.sampler.RandomSampler() ); // Create a statsD client that uses a persistent UDP socket (one that will remain // open for the lifetime of the statsD client, until .destroy() is called). client = new lib.client.StatsDClient( new lib.transport.PersistentUDPTransport(), new lib.sampler.RandomSampler() ); // Create a statsD client that will buffer messages until they reach a max length // of 500 characters, at which point they will sent out as a \n-delimited list. client = new lib.client.StatsDClient( new lib.transport.BufferedTransport( new lib.transport.UDPTransport(), 500 ), new lib.sampler.RandomSampler() ); // --- // Create a statsD client that doesn't actually send messages to statsD. Instead, // the messages will be captured in an internal queue that can be used to test the // throughput of the statsD client. transport = new lib.transport.CaptureTransport(); client = new lib.client.StatsDClient( transport, new lib.sampler.RandomSampler() ); // ... send messages. // Check to see that the client actually sent messages. writeDump( trasnport.getSentMessages() ); </cfscript>
This might seem really obvious to mature programmers; but, I have never felt very confident in my object design. So for me, breaking a component up and then seeing how much the modularity facilitates extending the functionality is an extremely rewarding process. For the first time in my life, I think I'm starting to understand the Single Responsibility Principle, which is of course, one of Uncle Bob Martin's SOLID principles.
Want to use code from this post? Check out the license.
Sorry for the pedantry but should 'trasports' be 'transports'?
I am rubbish at spelling though, so I'm probably wrong, or misreading.
Ugg, you are totally right :( Sometimes, I am baffled at how my spellchecker doesn't catch these things... or how my brain selectively ignores them :D
Fixed the title :) Thanks for the catch!
typo line 42 transport