Instance Variables in ruby
Dogs have many shared characteristics, like the abilities to wag their tails and drink water from a bowl, but they also have information about them that is variable,
like their breed or their name.
Similarly, when designing an application, your users will have common traits, like the ability to log in and out, but some information will be variable,
like a username or email address. Let’s start by setting up a new User
class. You can follow along in either irb or by running a script from the command line.
class User # stuff will go here end
By the end of this lesson, you’ll be able to define a User
class that can track its own user information.
Say you wanted to assign a user’s username. You might try something like this.
julia = User.new julia.username = "coolgirl2000" # NoMethodError: undefined method `username' for #<User:0x007fc6fa034148>
Let’s pause to take a look at that error bit by bit.
NoMethodError
: If I had to guess, I’d say this likely has something to do with a method not existing.undefined method 'username'
: Suspicions confirmed. It looks like our code can’t find a 'username' method. That makes sense, we never defined one.
But you weren't trying to create a new method! You were thinking about this username
like a variable, right?
Ruby has no way of distinguishing between variables and methods in this case, since they both come after the .
, so it only supports instance methods.
😱What do we do now?! To treat username
as a variable, you’re going to need to fake this a bit. Let’s write just enough code to stop this pesky error.
class User def username=(value) end end
If you recall, most characters are fair game with methods, including =
. In this case, the value
argument stands in place of the value you want to assign to username
.
julia.username=("coolgirl2000") # => "coolgirl2000"
That does the trick… sort of. There’s an awful lot of syntax here, and it’s a bit of a stretch to say this looks anything like the julia.username = “coolgirl2000”
we were going for.
Luckily, we can leave off the parentheses since those are optional for methods. These little shortcuts are known as syntactic sugar, since they make writing code with it just a bit sweeter 🍭. Let’s see how that looks.
julia.username= "coolgirl2001" # => "coolgirl2001"
Wow, I already feel so much less stressed looking at that. The fact that the =
is pressed up againstusername
is still bothering me. I can’t stop looking at it.
It’s going to bother me forever if you don’t do something about it. Please, do something about it.
Syntactic sugar to the rescue again. When a method ends with =
, Ruby rightfully assumes that you’re creating a method for variable assignment. To make your life sweeter, Ruby lets you put a space before the =
so it reads more like typical variable assignment, like so:
julia.username = "coolgirl2002" # => "coolgirl2002"
💥Boom. Check that out. I can sleep easily again. You’ve just successfully created (the start of) asetter method (sometimes called a mutator method).
A setter method is one which sets, or changes, the value of a variable belonging to a particular object.
Alright, let’s check in on your username
. If my math is correct, it should be set to “coolgirl2002”
right now.
julia.username # NoMethodError: undefined method `username' for #<User:0x007fc6fa034148>
This error again. You’ve seen this one before, but didn’t you already create this instance method?
Let’s go ahead and write the bare minimum amount of code to make this error go away.
class User def username=(value) end def username end end
This is the opposite of a setter method, a getter method. It exists to get the value of a variable belonging to an object. Let’s see if this works.
julia.username # => nil
There’s no error 🎉, but it’s also returning nil
instead of “coolgirl2002”
. You may have already caught on to this…
neither your username
nor your username=
methods actually do anything.
To make your getter and setter methods start working, you’re going to need to capture theusername
values somewhere.
The problem here has to do with scope. How can the values you capture in the username=
setter method be accessed in the username
getter method?
Much like instance methods, you can also use instance variables. Instance variables are variables that are scoped to the entire class.
This means that once an instance variable is defined, it can be used anywhere in that object.
An instance variable looks just like a regular variable, with one minor difference: it starts with @
.
class User def username=(value) @username = value end def username @username end end
Let’s see if this works before moving on.
julia.username = "coolgirl3000" # => "coolgirl3000" julia.username # => "coolgirl3000"
🙌 Nice!
The @username
instance variable is now available to use by any of the methods inside of a User
object.
You can go ahead and make other User
objects now, and each one will be able to track their own information.
walter = User.new # => #<User:0x007fc6fa034328> walter.username = "cooldude17" # => "cooldude17" walter.username # => "cooldude17"
And one last check with Julia to make sure her information is still accurate.
julia.username # => "coolgirl3000"
Remember when we said that $global_variables
weren’t the best solution? This is that best solution.
Very rarely will you need a variable that is available to everything inside your application. Often, you just want it available to one or a few objects.
Let me say that again: You will almost never need to use global variables ever again. If you find yourself using one,
there’s a very good chance it can be done some other way using classes.
Getter and setter methods are used so often in Ruby that there are even more shortcuts you can use to make your life sweeter.
Why type more code than you really need to? Remember, Ruby is optimized for developer happiness.
class User attr_reader :username attr_writer :username end
attr_reader
and attr_writer
(“attr” being shorthand for “attribute”) are two methods that take care of creating getter and setter methods, respectively, for you.
Best of all, you can do this for as many attributes as you’d like. Perhaps you want users to be able to change their usernames, but not their birthdays.
class User attr_reader :username, :birthday attr_writer :username end
There’s no need to actually write out all those getter and setter methods yourself anymore!
One thing that confused me at first was the syntax of attr_reader
and attr_writer
.
It didn’t really look like anything I’d ever seen before, at least not until someone rewrote it for me this way:
attr_reader(:username, :birthday)
These are actually instance methods that are already baked in for you to use.
All you are doing is passing in a Symbol representing the instance variable you’d like to create getter and setter methods for.
Accessors
There’s one last refactor we can do here. It’s pretty common to need both a getter and a setter method for an attribute, so why not define both at once?
That’s exactly what attr_accessor
does.
class User attr_accessor :username end
I know what you’re thinking. Yes, I did just string you along this emotional journey only to end up writing three lines of code.
But think about it: now you know what these three lines of code actually do!
Along the way, you learned about instance variables, some neat syntactic sugar, and getters and setters. Not bad.
Let’s continue with the User
example. This is what we’ve got at the moment:
class User attr_accessor :username end
This is useful, but presents a slight issue. We don’t want a user to ever be without a username.
The way things are currently set up, you can easily do something like this:
colt = User.new # => #<User:0x007fc6fb03ae98> colt.username # => nil
Because a User
is created without a default username
value, Colt is left without a username until he goes in and sets one himself.
🚫This will not do.
There’s a special instance method you can use to make sure an object is properly set up when it’s created, initialize
.
When you create a new object, the first thing Ruby will do is look for thatinitialize
method and run it.
This makes is super handy for initial setup (for things like… initializing an object with a username, maybe?).
class User attr_accessor :username def initialize @username = "its_colt_outside" end end
Let’s give this another go.
colt = User.new # => #<User:0x007fc6fb03ae98 @username="its_colt_outside"> colt.username # => "its_colt_outside"
💅Beautiful.
There’s just one issue with the previous example— we’re hard-coding “its_colt_outside”
to be the default username for every single user.
That’s not going to help you much when Orit wants to make an account for herself.
How about passing in the username as an argument right from the start, like a method, when creating the new User
object?
orit = User.new("supermom")
To do this, you’ll need to modify your User
class so that it can accept arguments. It may be tempting to do something like this:
class User(username) # ... end
But as you’ll quickly encounter, this will throw an error. This is a completely reasonable assumption, though.
Instead, you can include the argument as a part of the initialize
method.
A good way to remember this is to keep in mind that initialize
takes care of everything related to the initial set-up of an object.
Since the username
argument deals with the initial set-up of a username, it belongs to the initialize
method.
class User attr_accessor :username def initialize(username) @username = username end end
Now you can initialize a User
object with a username right when you create it.
orit = User.new("supermom") # => #<User:0x007fc6fb03ae98 @username="supermom"> orit.username # => "supermom"
This also safeguards you from creating a new user without a username.
mike = User.new # ArgumentError: wrong number of arguments (0 for 1)
If you break apart that error, it shouldn’t be too tough to figure out what the issue is.
ArgumentError
: This probably has something to do with the arguments passed into thisUser
object.wrong number of arguments (0 for 1)
: Zero arguments were passed in, when theUser
class is expecting one.
Errors can seem scary, but often they contain vital information to help fix bugs in your code. Good error messages will tell you exactly where you need to make a fix.