Two Designs for SequenceIdGenerator
In my previous, marathon post on id generation, I mentioned that I was generating unique sequence numbers. It was straightforward to write a class to generate them:
class SequenceIdExhaustion < RangeError ; end
class SequenceIdGenerator # has 14 bits allocated to it in the CallNumber MAX_SEQUENCE_ID = 2 ** 14 - 1
def initialize reset! end
def reset! @sequence_id = 0 end
def sequence_id raise SequenceIdExhaustion if @sequence_id > MAX_SEQUENCE_ID @sequence_id end
def consume_sequence_id! sid = sequence_id increment! sid end
private
def increment! @sequence_id += 1 sequence_id end end
Simple interface: start at zero, a method to consume an id, a method to reset the generator, and a reader to get the id. All while making sure to raise an appropriate exception when the generator has moved into an invalid state.
{.aligncenter .size-full .wp-image-2205 width=”589” height=”99”}
This object is all about maintaining state, so it’s mostly bang methods. That aside, I didn’t love this implementation.
There’s not really any need for the sequence_id
getter because that state of “what sequence id are you at now?” doesn’t and shouldn’t matter to any code. And that temporary variable in consume_sequence_id!
is just clunky.
So, second pass:
class SequenceIdGenerator # has 14 bits allocated to it in the CallNumber MAX_SEQUENCE_ID = 2 ** 14 - 1
def initialize reset! end
def reset! @sequence_id = -1 end
def consume_sequence_id! increment! @sequence_id end
private
def increment! @sequence_id += 1 raise SequenceIdExhaustion if sequence_id > MAX_SEQUENCE_ID end end
This has a smaller public interface, which is generally a win. I’ve cleaned up the that temporary variable in consume_sequence_id!
in an odd way: by starting in an invalid state.
No one from the outside can see that sequence_id
starts invalid, but this is still not OK. Every method potentially needs to account for this. Alternately I could restore the sequence_id
getter and have it raise if it’s accessed before consume_sequence_id!
has moved it to a valid state, but having a to protect a class from itself just feels wrong.
Here’s a change to remove that temporary variable nicely:
def reset! @sequence_id = 0 end
def consume_sequence_id! @sequence_id.tap { increment! } end
This uses the K combinator. It used to be implemented in Rails as returning
, then migrated into Ruby 1.9 as tap
. It’s usually used with the value passed into the block, but works fine without.
After that last long post, I wanted to share this nice little bit of OO design. This keeps state hidden while not succumbing to the temptation of allowing hidden state to be invalid.