Read my latest article: Planet Argon Blog (posted Wed, 17 Feb 2010 15:11:00 GMT)

Using model constants for project sanity

Posted by Robby Russell Tue, 23 Jun 2009 06:39:00 GMT

24 comments Latest by wholesale ed hardy Wed, 17 Mar 2010 07:21:50 GMT

On one of our larger client projects (approx. 160 models and growing…) we have a specific model that we refer to quite a bit throughout our code. This model contains less than 10 records, but each of them sits on top of an insanely large and complex set of data. Each record refers to a each of their regions that our client does business in.

For example… we have, Australia, United Kingdom, Canada, United States, and so forth. Each of these regional divisions has their own company code, which are barely distinguishable from the next. They make sense to our client, but when we’re not interacting with those codes on a regular basis, we have to look constantly look them up again to make sure we’re dealing with the right record.

I wanted to share something that we did to make this easier for our team to work around these codes, which we should have thought of long ago.

Let’s take the following mode, Division. We only have about 10 records in our database, but have conditional code throughout the site that are dependent upon which divisions specific actions are being triggered within. Each division has various business logic that we have to maintain.

Prior to our change, we’d come across a lot of code like:

# For all divisions except Canada, invoices are sent via email
# In Canada, invoices are sent via XML to a 3rd-party service
def process_invoices_for(division)
  if division.code == 'XIUHR12'
    # trigger method to send invoices to 3rd party service
    # ...
  else
    # batch up invoices and send via email
    # ...
  end
end

An alternative that we’d also find ourselves using was.

if division.name == 'Canada'

Hell, I think I’ve even seen if division.id == 2 somewhere in the code before. To be fair to ourselves, we did inherit this project a few years ago. ;-)

Throughout the code base, you’ll find business rules like this. Our developers all agreed that this was far from friendly and/or efficient and worst of all, it was extremely error-prone. There have been a few incidents where we read the code wrong and/or got them confused with one another. We were lacking a convention that we could all begin to rely on and use.

So, we decided to implement the following change.

Model Constants

You might already use constants in your Ruby on Rails application. It’s not uncommon to add a few into config/environment.rb and call it a day, but you might also consider scoping them within your models. (makes it much easier for you to maintain them as well)

In our scenario, we decided to add the following constants to our division model.

class Division < ActiveRecord::Base
  AFRICA      = self.find_by_code('XYU238')
  ASIA        = self.find_by_code('XIUHR73')
  AUSTRALIA   = self.find_by_code('XIUHR152')
  CANADA      = self.find_by_code('XIUHR12')
  USA         = self.find_by_code('XIUHR389')
  # etc..
end

What this will do is load up ech of these constants with the corresponding object. It’s basically the equivallent of us doing:

if division == Division.find_by_code('XIUHR389')

But, with this approach, we can stop worrying about their codes and use the division names that we’re talking about with our clients. Our client usually approaches us with, “In Australia, we need to do X,Y,Z differently than we do in the other divisions due to new government regulations.”

if division == Division::CANADA
  # ...
end

case division
  when Division::AFRICA
    #
  when Division::AUSTRALIA
    # ...
end

We are finding this to be much easier to read and maintain. When we’re dealing with a lot of complex business logic in the application, little changes like this can make a big difference.

If you have any alternative solutions, we’d love to hear them. Until then, we’ve been quite pleased with this approach. Perhaps you’ll find some value in it as well.

Subscribe to my RSS feed Enjoying the content? Be sure to subscribe to my RSS feed.
Comments

Leave a response

  1. Avatar
    Neil Tue, 23 Jun 2009 07:07:46 GMT

    Hi Robby

    Would it be possible to extract the country codes out and use a method that determines whether or not a region code record wants invoices emailed right away or by third party? E.g.

    if division.needs_invoices_processing?

    But perhaps it’s a bad suggestion if you won’t be adding any more region codes or if all region codes need different behaviour from the rest….

  2. Avatar
    Robby Russell Tue, 23 Jun 2009 07:12:27 GMT Recommend me on Working with Rails

    Neil,

    Yeah, we do that quite a bit as well. There are some cases where we’re just doing something quirky (like changing copy in a flash message for one specific division, handling different tax rates, etc.). Using methods like you’ve suggested comes in handy, but mileage varies. :-)

  3. Avatar
    Nathan de Vries Tue, 23 Jun 2009 08:27:45 GMT

    What about using ActiveSupport::StringInquirer to achieve something like “division.name.australia?” or “division.name.usa?”?

  4. Avatar
    shane Tue, 23 Jun 2009 08:50:48 GMT

    First thought as I skimmed the article … often when I see ‘if’ statements I wonder about refactoring to move the behaviour to the objects … so you can just call division.process_invoices.

  5. Avatar
    Emilien Tue, 23 Jun 2009 08:59:53 GMT

    Hello Robby, What about using the “type” magic column and processing the custom behaviours in subclasses ?

  6. Avatar
    Morgan Roderick Tue, 23 Jun 2009 09:48:35 GMT

    If you’ve got such a short list and mostly use it for branching in your code, perhaps a few instance methods would go a long way towards (even more) readable code?

    
    if division.canada?
    # canada specific stuff
    end
    
    

    Would that work with case statements?

  7. Avatar
    Peter Harkins Tue, 23 Jun 2009 12:14:20 GMT

    Maybe you have other reasons to keep doing things this way, but it sounds like case where Tell, Don’t Ask would improve your code. In this situation, I’d probably give a Division an after_initialize to include a country-specific module of code.

    Or at the least, write ‘if division.invoice_by_email? ...’ because that’s the important question you’re asking that location is only a proxy for.

  8. Avatar
    Robby Russell Tue, 23 Jun 2009 14:34:36 GMT Recommend me on Working with Rails

    With regard to moving some of this into individual modules (per-division), one concern with this is that it makes it more difficult to look over the business rules for all the divisions within a specific method being called.

    For example, take the invoicing functionality.

    In our client application, we actually have rules like this:

    if Canada or USA
      # send via XML to 3rd party service (we call another method here)
    elsif Australia or New Zealand
      # build CSVs and FTP them
    else # rest of divisions
      # send emails
    end

    If we moved these down into modules (and separated them out), it’d make it much more daunting to check on an issue if we had to sift through multiple areas of code to figure out which regions were doing what and how.

    We do have a lot of methods in our Division model that we rely on for determining stuff, but still find ourselves needing to do comparisons in various places of the application.

  9. Avatar
    Chris Gunther Tue, 23 Jun 2009 18:02:35 GMT

    Hey guys,

    Nice article. I had a question about performance though. How often are those constant definitions called? Since in a production environment the classes are cached, would it be executed once then live until the server is restarted?

    Thanks, Chris

  10. Avatar
    Peter Harkins Tue, 23 Jun 2009 19:04:13 GMT

    Why do you find yourself looking over the code to compare divisions? Could you write a tool to extract the ‘process_invoice’ someone suggested to display them all side-by-side?

    I just really don’t like spreading logic out from the object into other scripts. Every time I’ve done it I’ve later realized that my code is clunky or hard to test or flaky because half the code for an object lives outside that object, making it harder to test and reason about.

    In any case, this issue is tangential to your post, which is quite nice, and I don’t mean to be the “ur doin it rong!” guy. Thanks for sharing it.

  11. Avatar
    Tal Rotbart Wed, 24 Jun 2009 03:36:57 GMT

    Without diving into patternitis, this sounds like a case for the Strategy pattern

    class Division 
    ...
    
      def process_invoice
        invoice_strategy.perform(self)
      end
    
    ...
    end
    
    With invoice_strategy being either:
    1. a simple attr_accessor method which contains an InvoicingStrategy instance that was chosen when you initialised the division constant, or
    2. a method which uses logic to decide which kind of invoicing strategy is required for that specific division (and/or even that request).

    This way the code for processing invoices by email is kept as one (testable) class while the code for processing invoices by rube-goldberg machine is kept separately as one (testable) class, etc.

  12. Avatar
    grosser Wed, 24 Jun 2009 05:42:13 GMT
    We got the same problem here, but resolved it using something like
    
        %w[foo bar foz].each{|name| define_method(name+'?'){ region == name }}
    
  13. Avatar
    Guoliang Cao Wed, 24 Jun 2009 12:36:31 GMT

    We are in similar situation here. In our application, we have to support several countries (US, CA, UK) and in many places, the behavior is slightly different. When those are initially implemented, we used if Locale.current.canada? to encapsulate the logic. Later I created a LocaleAware class and LogicForDefault, LogicForCanada classes. So I could use if LocaleAware.support_cpni? and have all canada specific logic in one place to look and change. This has worked well in many cases. There are still some cases we feel it is probably better to leave the “if country” there though.

  14. Avatar
    Rob Sanheim Wed, 24 Jun 2009 13:29:20 GMT

    I’m actually going to comment on the specific technique you talk about here, as opposed to the overall design. :)

    I’ve been burned in the past by defining top level constants in models that do ActiveRecord finds. The reason is that those finds get executed whenever the class gets loaded, instead of later on when the constant is actually needed. This means that your app ends up calling the database at times when you don’t expect it – for example when executing any rake task that happens to end up referring to Division.

    This can also bite you if you ever want to decouple your app more from the database, in order to use something like UnitRecord for example. Oh, it also means its really hard to stub or mock those values in tests as well (hello Object.const_set nastiness as opposed to Division.stubs(:usa) )

    I usually go right to class methods in approaches like this. Its just as clear, and its much more flexible – you can memoize it (or not), you can stub/mock like any other methods, and they don’t get loaded at class eval time.

  15. Avatar
    Geoff Buesing Wed, 24 Jun 2009 15:06:00 GMT

    I share Rob Sanheim’s concerns about loading models when the class gets loaded. Would be cleaner to just set the constants as the codes, like so:

    
    class Division < ActiveRecord::Base
      # country codes
      AFRICA = 'XYU238'
      ASIA = 'XIUHR73'
      ...
      # rules for who gets what feed
      WANTS_XML_FEED = [USA, CANADA]
      WANTS_CSV_FEED = [AUSTRALIA, NEW_ZEALAND]
    

    ...and then instance methods can check against these codes:

    
    def wants_xml_feed?
      WANTS_XML_FEED.include? code
    end
    
    def wants_csv_feed?
      WANTS_CSV_FEED.include? code
    end
    

    Controller methods then don’t have to contain rules about who gets what feed type, they just need to deliver as appropriate:

    
    def feed
      ...
      if division.wants_xml_feed?
        # deliver xml
      elsif divison.wants_csv_feed?
        # deliver csv
      else
       # send email
      end
    end
    
  16. Avatar
    Matt Wynne Sat, 11 Jul 2009 19:39:42 GMT

    This code, to me, is absolutely crying out for a little polymorphism. If there are only ten of these divisions, why even keep them in the database? Do their attributes ever need to be changed at run-time?

    Mightn’t it be easier to just have POROs like

    Division::Canada Division::UK Division::Africa

    etc and put the specific behaviour into each concrete class?

    I know what you mean about being able to see all the behaviour of each different division side-by-side, but this kind of conditional logic is soooooo prone to errors. See http://www.antiifcampaign.com/ if you want to learn more.

  17. Avatar
    Philip Orwig Sat, 11 Jul 2009 19:46:06 GMT

    Just be aware that this method won’t work with transactional fixtures unless you have them preloaded—ActiveSupport::TestCase loads model class when you make a call to “fixture”, and doesn’t populate the tables until right before running a test case.

  18. Avatar
    susan Wed, 10 Feb 2010 12:42:12 GMT

    Furthermore, although they are also used by the user to keep feet warm between discount ugg cardy boots waves catch should not be used to get into the waves. In fact, Ugg boots should be worn to be in other conditions of humidity, mud, rain, ice or mud. discount ugg boots To avoid unexpected weather damage Ugg, you should apply repellent thoroughly with the frequency prescribed. This will seal the sheep’s clothing, dirt and stains well. wholesale ugg boots The best product for this is Australia sheepskin UGG water and stain.

  19. Avatar
    ed hardy clothing Wed, 10 Mar 2010 07:09:57 GMT

    I can always get much useful information here. I hope I can know more information here. Many thanks.

  20. Avatar
    ed hardy clothing Wed, 10 Mar 2010 07:16:24 GMT

    Nice post. Hope to see more fresh ideas here. Thanks a lot.

  21. Avatar
    abercrombie clothes Wed, 10 Mar 2010 07:58:09 GMT

    thanks a lot for sharing and hope more

  22. Avatar
    www Wed, 10 Mar 2010 08:51:38 GMT
  23. Avatar
    pandora jewelry Wed, 17 Mar 2010 06:53:33 GMT

    nice post

  24. Avatar
    wholesale ed hardy Wed, 17 Mar 2010 07:21:50 GMT

Share your thoughts... (really...I want to hear them)

Comments