Ruby OOP Part 2 — Exploring Instance & Class Variables, Methods, Scopes, and Self
Instance variables are differentiated from other variable types by prepending an @
to the beginning of their name. They are used to tie individual attributes for a specific instance of the class that names them, but each object created from that class creates its own copy of those instance variables. The values assigned to instance variables contribute to the overall state of the object. Thus, outside of the many commonalities that may be gained from a shared class, they allow us to track an individual object’s unique state.
Since instance variables track the state of individual objects, they are scoped at the object level and cannot cross over between different objects. However, they are available throughout every instance method defined in their class. Let’s take a look at the example below.
In this code snippet, we define a Student
class from lines 1 to 10. The scope of its instance variables (where they can be accessed) is within this entire section of code. Therefore, we don’t need to pass them as parameters in any instance method we define. This is demonstrated in our #greet
method above, which has access to the instance variables @name
and @city
despite having been initialized outside of that method.
Instance variables can also be accessed prior to being initialized. In this case, Ruby recognizes it as a variable that references a value of nil
. This is in contrast to local variables, which would raise a NameError
.
The Student
class is defined such as the @student_id
instance variable is not initialized at the time of object creation. This is demonstrated on line 18, which returns nil
, and then on line 19 when we the inspection of the rob
object appears without the @student_id
attribute. Once we assign a value to @student_id
by invoking the #student_id=(value)
setter method on line 21, a description of our rob object with @student_id
and its value gets printed on line 22.
You may have noticed that the @city
instance variable does appear when we inspect the rob
object on line 19, despite having a value of nil
. This is in contrast to @student_id
, which only appears after we invoke its setter method. This shows us that instance variables only truly become a part of an object’s attributes once a value has been explicitly assigned to them.
Instance variable scope in class inheritance
A superclass conveys attribute names and methods to all of its subclasses. The subclass then uses those names as a reference to point to a value. The values that are referenced by those attributes are not inherited, even if they get initialized within a parent class method. The instance of the class that calls the setter method is the one that owns that attribute name:value pair.
In the code above, this is demonstrated in the Student#greet
method, which outputs “Hi, my name is Jack”. Instance variables are scoped at the object level, and belong to the class in which they were initialized — even when we initialize them using a method that was inherited from a parent class. For example, the Person#goal
method above has access to the instance variable @goal
of the Student
object jack
and outputs a unique string specific to that instance.
Instance variables and Modules
Instance variables can also be initialized and inherited from modules. However, as compared to classes, modules do not have an #initialize
method that gets called automatically upon object creation (modules cannot instantiate objects). In this case, the instance variable must be initialized by explicitly invoking the setter method defined in the module like so:
When the duck
object invokes the #set_sound
instance method defined in the Speakable
module, @sound
is initialized and tied to the duck
object. It is only after that method invocation that the instance variable becomes available — as shown with the#speak
method invocation, which outputs “Duck quacks!”.
Instance Methods
Instance methods are methods defined within a class that describe the behavior available to all instances of that class. Instance methods have access to instance variables due to their scope, so they can be used to track and manipulate data relating to a particular object’s state. As seen above, instance methods will have differing outputs depending on the calling object and the individual object’s state. Therefore, instance methods that do not incorporate instance variables within their execution will output/return the same result for all objects instantiated from that class.
Getter Methods
Outside of the class, we need a specially defined method to access the values stored within the instance variables associated with an object. We can define this method within the class, so that we can retrieve the value in question wherever the object is accessible. In the code below, two identical getter methods are defined.
The first one, in the Pokemon
class, defines a simple getter method. Although we could name these methods anything we want, by convention, we define our getter methods with the same name that was used for the instance variable. The method’s implementation simply returns the instance variable’s value.
The second one, in the Poke
class, is Ruby’s shorthand/shortcut notation for a getter method. Attribute reader methods are implemented in exactly the same way as the getter method that was defined in the Pokemon
class. We simply write attr_reader
followed by a symbol representation :name
of the instance variable. You can also chain getter methods for additional instance variables like so: attr_reader :name, :age, :gender
It is good practice to use a getter method to access an instance variable’s value rather than using the instance variable directly. If no getter methods were defined, we would get a NoMethodError
. This helps avoid debugging headaches in case you mistype an instance variable’s name, which would return nil
instead of raising an error. That way, you allow your code to remain flexible and easier to maintain since changes would only need to be made where the getter method is defined. For example, have a look at the code below and the outputs at the end:
Setter methods
Setter methods allow us to initialize and reassign attributes for a particular object. In other words, they allow us to set or alter the data stored within an instance variable. They are defined similarly to getter methods, but commonly include parameter reassignment within the method definition. You’ll often come across them with an =
after the method name, which differentiates them from getter methods, while allowing us to take advantage of Ruby’s syntactical sugar.
In the following code snippet, two common errors that arise when defining setter methods are demonstrated:
Error 1: On line 21, we invoke the setter method defined on line 10 #set_author=(value)
. Within the method, although a getter method has been defined for author
, Ruby mistakes this for local variable initialization. This is demonstrated by its output “The author is Jules Verne” followed by a nil
return when we print book.author
.
Error 2: On line 23, we invoke the setter method defined on line 15 #author=(value)
, which attempts to assign the value "Jules Verne” with self.author = value
. In this case, self
refers to the instance (or object) on which the method is being called — this means self.author = value
is exactly the same as author=(value)
. For that reason, our second setter method gets recursively called and Ruby quickly raises aSystemStackError
to help us realize our mistake.
To avoid those tricky errors from happening, it is important to be explicit in our setter methods by either:
- Directly referencing the instance variable using its prefix
@
- Defining a custom setter or
attr_writer
method and then calling one of them usingself
from inside another instance method (making sure it’s named differently than our main setter method)
attr_accessor
To keep our code even more concise, and in cases where we don’t need to implement any additional functionality to our getter or setter methods, we can replace attr_reader
& attr_writer
with attr_accessor
. This provides us with getter and setter methods in one line of code.
Deciding on which to use in your program will depend on the level of access you want to allow for those attributes/methods outside of your class. For example, you may allow objects of a Student
class to ask for a name
but not allow them to alter its value after object instantiation. In this case, you would define an attr_reader
without a setter method for @name
. When dealing with sensitive information, such as a social security number, you’ll likely want to prevent that attribute from being accessed or viewed from outside the class. In this case, you could define a private attr_accessor
, or a private attr_writer
and no attr_reader
, or forego both and only allow that attribute to be set at the time the object is created.
Class Variables
Class variables are variables that are available to the class itself and to every object created from that class. They contain information that is common across all instances of a class — they are pertinent to the class as a whole rather than being tied down to a specific instance of a class. Class variables are created by prepending @@
to their name. They are scoped at the class level and are accessible within all instance and class methods defined in that particular class. All instances share one copy of the class variable, which means reassigning its value in one instance will also be evident in every other instance.
While instance variable values are not inheritable, class variables and their values are, regardless of how many subclasses there may be.
Class Methods
Class methods are a type of method that we call directly on the class itself. This means that we do not have to instantiate objects prior to invoking them, since they pertain to the class as a whole. Instances do not have access to class methods, just like the class itself cannot access instance methods. To define a class method, we prepend the method name with the keyword self
, which refers to the class name in this case.
Inside a class method’s implementation, we are able to invoke other class methods without explicitly using the keyword self
. For example, on line 18, Ruby knows we’re inside a class method and using self
in this case would refer to the class itself — Weather
. Since we didn’t pass city
as an argument nor initialize a city
local variable within the method, Ruby looks for a class method with the same name, which it does (self.city
on line 5).
Unlike instance variables, class variables do not have a shorthand notation syntax for getter and setter methods. I can only assume that it’s because class variables are rarely used amongst Rubyists.
self
Everything in Ruby is an object. Therefore, every line of code you write belongs to an object. self
is a special keyword that points to the object that owns the current code being executed. Depending on where that line of code is, self
will contextually refer to a particular object, which may be a class, a module, an instance or something else. In fact, you can pretty much use it anywhere. Let’s try it out.
In cases of class inheritance, when a child class inherits both instance and class methods from a parent class, the call to self
always refers to the subclass’ instance when invoking instance methods, and to the subclass’ class itself when invoking class methods. No matter which context you find yourself in, remember that self
always refers to a single object at any given time. What is interesting is that if we invoke self.class
in a top level (main) context, Ruby returns Object
. This means that as soon as we begin writing code, Ruby initializes an instance of Object
, which it calls main
by default.
Summary
Here are a few key points to remember about instance and class variables
- Instance variables / methods are scoped at the object level.
- Class variables / methods are scoped at the class level.
- Instance variables are accessible in instance methods only.
- Class variables are accessible in instance and class methods.
- Instances of a class gain unique copies of every instance variable and share a single copy of any class variable.
- attr_* shortcut methods are only available for instances.
- Instance variable values are not inherited.
- Class variable values are inherited.