Two Designs for SequenceIdGenerator
« Distributed ID Generation and Bit Packing
» 2014 Book Reviews
Code: Chibrary, design, object orientation, Ruby, state
Comments Off on 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.
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.