2

I have a non-database-backed class in Ruby:

class User
  attr_accessor :countries
end

I want countries to simply be an array of ISO country codes (US, GB, CA, AU, etc) and I don't want to build a separate model to hold each. Is there a magic way to make Ruby understand that :countries is an array and treat it accordingly, or do I need to write the countries and countries= methods?

I tried just setting the countries array with user.countries = ['US'], and I'm getting a NoMethodError.

8
  • 5
    It's an accessor; it doesn't matter what type it is. Commented Sep 24, 2013 at 15:40
  • If you use attr_accessor this, you will get free countries and countries= :) :) Commented Sep 24, 2013 at 15:42
  • How does the class need to "treat it accordingly" that is different to just user.countries = [:gb, :au]? Commented Sep 24, 2013 at 15:46
  • 1
    attr_accessor provides the countries and countries= methods, but there is nothing to stop you setting @countries to something other than an array. If you want to do array things, like << and length to it then you need to write your own methods. Commented Sep 24, 2013 at 15:46
  • 1
    it seems like your question is already more or less answered. But this use case that you've indicated seems more suited for a constant rather than an accessor method, imho. Commented Sep 24, 2013 at 15:51

2 Answers 2

6

The type of a variable doesn't matter in Ruby.

attr_accessor just creates getter and setter methods that set and return instance variables; @countries in this case. You can set the instance variable to your array, or use the setter:

class User
  attr_accessor :countries

  def initialize
    @countries = %w[Foo Bar Baz]
    # Or...
    self.countries = %w[Foo Bar Baz]
  end
end

> puts User.new.countries
=> ["Foo", "Bar", "Baz"]

Personally I prefer using the instance variable instead of self.xxx; it's too easy to forget the self. bit and you end up setting a local variable, leaving the instance variable nil. I also think it's ugly.

If the countries won't be changing between instances, why not a constant?

Edit/Clarification

Tadman's point is well-taken, e.g., this diatribe on state. The circumtances under which I don't care about that are limited to small, self-controlled, stand-alone classes. There are inherent risks in making those assumptions, the level of those risks is project-dependent.

Sign up to request clarification or add additional context in comments.

5 Comments

Using the instance variable when you have an attr_accessor can lead to some slip-ups if you've overridden either of the getter or setter methods. If you do a lot of JavaScript where this is absolutely required, being disciplined about adding self isn't a big deal.
@tadman Correct. If you're using attr_accessor and overriding one or the other, I'd say you're doing it wrong. IMO attr_accessor communicates that it's an accessor, not a method that performs other logic.
Well, it could be overridden in a subclass but not in the parent class. It's kind of risky to make assumptions. If you declare any kind of accessor, you should probably use it even if the instance variable is probably the same.
What if I want to add elements to the array programatically? self.countries.push("bla") won't work
@TheRookierLearner I don't know why you think that.
4

Looks like countries should be a constant:

class User
  COUNTRIES = %w(
    AF AX AL DZ AS AD AO AI AQ AG AR AM AW AU AT AZ BS BH BD BB BY BE BZ BJ BM
    BT BO BQ BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM
    CG CD CK CR CI HR CU CW CY CZ DK DJ DM DO EC EG SV GQ ER EE ET FK FO FJ FI
    FR GF PF TF GA GM GE DE GH GI GR GL GD GP GU GT GG GN GW GY HT HM VA HN HK
    HU IS IN ID IR IQ IE IM IL IT JM JP JE JO KZ KE KI KP KR KW KG LA LV LB LS
    LR LY LI LT LU MO MK MG MW MY MV ML MT MH MQ MR MU YT MX FM MD MC MN ME MS
    MA MZ MM NA NR NP NL NC NZ NI NE NG NU NF MP NO OM PK PW PS PA PG PY PE PH
    PN PL PT PR QA RE RO RU RW BL SH KN LC MF PM VC WS SM ST SA SN RS SC SL SG
    SX SK SI SB SO ZA GS SS ES LK SD SR SJ SZ SE CH SY TW TJ TZ TH TL TG TK TO
    TT TN TR TM TC TV UG UA AE GB US UM UY UZ VU VE VN VG VI WF EH YE ZM ZW
  ).freeze
end

User::COUNTRIES.include? "US" #=> true

freeze prevents modifications:

User::COUNTRIES.delete "US"   #=> RuntimeError: can't modify frozen Array

Update

The problem here is that your countries array has to be persisted somehow. You are mentioning has_many so Rails seems to be involved. You can use ActiveRecord's serialize method:

class User < ActiveRecord::Base
  serialize :countries
end

This will save the countries attribute to the database as an object and retrieve it as such:

u = User.new
u.countries = ["US", "CA"]
u.save

u = User.last
u.countries
#=> ["US", "CA"]

It's converted to and from YAML internally, so the users table looks like:

mysql> SELECT * FROM users;
+----+-------------------+---------------------+---------------------+
| id | countries         | created_at          | updated_at          |
+----+-------------------+---------------------+---------------------+
|  1 | ---\n- US\n- CA\n | 2013-09-24 18:24:03 | 2013-09-24 18:24:03 |
+----+-------------------+---------------------+---------------------+
1 row in set (0,00 sec)

2 Comments

You can also make a class method to wrap this up: def self.countries; COUNTRIES; end. This might seem pointless, but it allows subclasses to override that method if required and avoids exposing the structure of this constant to other parts of your code that need access to an array.
See comment above. Users should actually have ownership over a subset of the full list of ISO codes. Has_many is overkill for my application.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.