A Post Entitled Constant-ly?
I’ve gone back and forth about what should constitute a good use of class-level constants and a recent project has brought this question up once again. In this project there are basically two different uses for class-level constants. The first, and less frequent, use of class-level constants serves to identify “magic values”. The second use is to define a closed set of options.
Class Constants as Magic Values
“Magic values”, if you are not familiar with the term, are those values that require you to change the response of a class or class instance to a message. Usually you find them in flow control logic such as if-then or case statements.
class AtBat
MAX_BALLS = 4
MAX_STRIKES = 3
attr_reader :pitches, :result, :strikes, :balls
def initialize(first_pitch = nil)
@balls = @strikes = 0
@pitches = [ ]
@result = nil
add_pitch( first_pitch ) unless first_pitch.nil?
end
def add_pitch(pitch_result)
@pitches ||= [ ]
@pitches.push pitch_result
case pitch_result
when :foul
add_strike if @strikes < MAX_STRIKES-1
when :ball
add_ball
when :strike
add_strike
else
@result = pitch_result
end
end
def active?
!@result.nil?
end
private
def add_strike
if active?
@strikes += 1
@result = :strike_out if struck_out?
end
end
def struck_out?
@strikes == MAX_STRIKES
end
def add_ball
if active?
@balls += 1
@result = :walked if walked?
end
end
def walked?
@balls == MAX_BALLS
end
end
In the code above the class-constants MAX_BALLS and MAX_STRIKES are magic values. They represent different “high water marks” for the AtBat class. Each instance should respond differently when a pitch is a ball and there are less than MAX_BALLS of them in the AtBat.
This is the best, and possibly only, justifiable use of class constants. Using constants in this case accomplishes two important objectives. First, it pulls the magic values into one place so that they can easily be maintained. If the rules of baseball changed to permit more balls or strikes in at bat you simple change the class-constant and you’re done. Second, the class constant is more expressive of the intent than the value. Why use “3” when evaluating the number of strikes? Because that’s the maximum allowed.
Class Constants as Closed Options
A special case of magic values is when the constant represents a closed set of options for the class. For example, consider when my favorite baseball team, the Boston Red Sox, conducts a contest during the season to give away a pair of tickets to a game and then does something I detest: they restrict entry to residents of the New England states. (Yes, I’m crazy enough to drive the thousand miles between my house and Fenway to see a game.)
class Entrant
ELIGIBLE_STATES = %w{CT MA ME NH RI VT}
attr_accessor :first_name, :last_name, :dob, :email, :street, :city, :state_abbrev
def valid?
new_england_resident? and over_18?
end
private
def new_england_resident?
ELIGIBLE_STATES.include? @state_abbrev
end
def over_18?
@dob < 18.years.ago
end
end
In the code above the ELIGIBLE_STATES constant is used as a special type of magic value. The instance reacts differently based on whether or not one of the attributes “matches” the constant. To that extent the closed option type of class constant makes some sense. It does often have an irritating side effect (constant redeclaration warnings when restarting a Rails application) but that’s the main downside.
More than Magic?
The problem that I have seen fairly often is that these class constants that begin as a closed set of options tend to evolve over time. The list of states shown above, for example, may take on an additional property for reporting purposes such as a state-specific tax rate for reporting the tickets as “income.”
class Entrant
ELIGIBLE_STATES = {:CT=>0.07, :MA=>0.09, :ME=>0.07, :NH=>0.0, :RI=>0.07 :VT=>0.09}
attr_accessor :first_name, :last_name, :dob, :email, :street, :city, :state_abbrev
def valid?
new_england_resident? and over_18?
end
def extended_ticket_value( ticket_price, qty )
qty * ticket_price * (1 + ELIGIBLE_STATES[@state_abbrev.to_sym])
end
private
def new_england_resident?
ELIGIBLE_STATES.include? @state_abbrev
end
def over_18?
@dob < 18.years.ago
end
end
Before you know it there are new “constants” springing up to deal with other flow control changes related to the first constant. Tax rates, shipping charges, and who knows what else might change based on the state of residence for the Entrant. This is where the problem lies. Because the needs emerged slowly they feel like a simple, organic extension of the class. But they’re not.
They are classes in and of themselves.
These classes are not constants in the traditional, magic value sense. They are classes with constant data. That is, they are classes whose data is set and known at the time the software is developed. Just like other classes, they deserve their own class body.
class State
NEW_ENGLAND_STATES = %w{CT MA ME NH RI VT}
attr_reader :state_abbrev, :tax_rate, :shipping_charge
def initialize(state_abbrev, tax_rate, shipping_charge)
@state_abbrev, @tax_rate, @shipping_charge = state_abbrev, tax_rate, shipping_charge
end
def new_england?
NEW_ENGLAND_STATES.include? @state_abbrev
end
def find_by_abbrev( abbrev )
@@states.detect{|state| state.state_abbrev == abbrev}
end
@@states = {
new(:CT, 0.07, 1.50),
new(:MA, 0.09, 0.50),
new(:ME, 0.07, 1.75),
new(:NH, 0.00, 1.75),
new(:RI, 0.07, 0.75),
new(:VT, 0.09, 2.50),
new(:SC, 0.07, 4.00)
}
end
class Entrant
attr_accessor :first_name, :last_name, :dob, :email, :street, :city, :state_abbrev
def valid?
new_england_resident? and over_18?
end
def extended_ticket_value( ticket_price, qty )
qty * ticket_price * (1 + state_of_residence.tax_rate)
end
private
def new_england_resident?
state_of_residence.new_england?
end
def state_of_residence
State.find_by_abbrev( @state_abbrev )
end
def over_18?
@dob < 18.years.ago
end
end
By pulling this class of constant data out as a unique class we gain similar advantages to the use of constants as magic values. The class now encapsulates all the knowledge about how to deal with a particular state and properly separates those concerns from how to deal with an Entrant. The Entrant code is even more expressive of its intent (e.g., state_of_residence.tax_rate vis-a-vis ELIGIBLE_STATES[@state_abbrev.to_sym]).
Admittedly, something very similar to this could be accomplished with a database-backed class like an ActiveRecord or MongoMapper derived class. The constant nature of the data, however, always makes that feel a little improper even if the end user has no access to the maintenance function. In the back of my mind I always worry that a necessary record will be inadvertently dropped so I want to ‘freeze’ it in code.
What about you? How do you deal with these kinds of classes of constant data? Do you recognize them up front? Shuttle them off to the database?