Ruby OOP Part 2 — Exploring Instance & Class Variables, Methods, Scopes, and Self

Marwan Zaarab
8 min readMay 24, 2022

--

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:

  1. Directly referencing the instance variable using its prefix @
  2. Defining a custom setter or attr_writer method and then calling one of them using self 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.

--

--