A Post Entitled Constant-ly?

posted 1 year ago in ruby

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?

Comments

About this site and its Author


The Tumblrs to Follow

  • staff
  • cdmwebs
  • paulsullivanjr
  • dawnvanasse
  • bitbltr