Software engineering can be confusing at times as languages and frameworks often use multiple terms to describe the same concept. When we describe a container possessing certain functions and variables, we are referring to the idea of encapsulation. While encapsulation is a universal term, certain languages refer to this idea as context or scope.
While the idea of a container having methods and variables is straightforward, it gets a little more complex when we introduce the idea of nesting. With nesting a container can possess one or more containers that can then possess their own containers and so on. Nesting gets complicated as we have to answer the question of who owns and has access to which variables and methods.
Along with answering who owns and has access to which variables and functions, we also need to answer what values or functionality we are accessing from different nested encapsulations or scopes. While we can’t have multiple functions or variables with the same name in the same encapsulation, we can share names between parent and child containers and even sibling containers.
This can get complicated and cause a lot of issues. From accessing the wrong value to produce the wrong result, to fully crashing our app when a resource can’t be located, not knowing where or what we are calling introduces a lot of issues. We need to be explicit in what we are calling, but how do we do that?
What is the Scope Resolution Operator?
To be able to ensure we are calling the variable or function we want to, we need to be explicit in how we call it. With multiple levels of nesting for our encapsulation there is not a declarative way to say “call variable X’s value in the second level of containers.” Instead we need to create a path telling Ruby imperatively how to get from the global, or the outermost container, to our desired container.
In order to do this we can string together module or class names using the Ruby scope resolution operator “::”. This operator lets us declare a path starting from the global container, down to our desired container (though we are able to declare abbreviated “local” paths for items in shared scopes). Once we have declared the path, we can access any variable or function with assurances we will get the correct value or functionality.
Say that we are writing a module to model mediums of music that we name “Music.” This module includes two nested modules “Record” and “EightTrack”. These are two mediums of how we may share music. However, in the same global scope we have a module “Record” which refers to how we persist items in our database. How do we ensure that we are accessing the correct modules:
We can be explicit in referencing our nested Record module from outside the module by calling the explicit path “Music::Record”. If we wanted to access our nested Record module in our “Music::EightTrack” module we can just call “Record” as both “Record” and “EightTrack” share the same local scope, “Music”, and therefore know where each other are located.
However, if we wanted to access the non nested “Record” module inside our nested “Music::EightTrack” module we would need to preface our call with what’s called a global scope resolution operator. This would look like “::Record”. Placing this operator at the front of a module or class call forces Ruby to begin the path from the global, or outermost, level of encapsulation instead of assuming local scope as the starting position.
Because our “Record” module for interacting with the database does not share scope within the music module “Music” with the module “EightTrack,” we need to explicitly declare the container it is contained in. This is why we end up using the global scope as its container is the global container.
By calling “::Record” inside of “Music::EightTrack” we are able to go to the top level, global, scope that both the “Record” and “Music” modules share and therefore use the correct reference. If we use this code, we will access the correct “Record” module and avoid any module not found errors or incorrect functionality.
How Ruby on Rails uses Scope
The Ruby on Rails framework prides itself on being a convention over configuration framework. In basic terms, Rails will do the setup for your project as long as you adhere to their house rules. These rules include class naming conventions and structuring the directories in a way that Rails can automate scope traversal and generating routes.
Now that we understand the scope resolution operator as a way to declare explicit paths to the resources we want, certain naming conventions and directory structures in Rails should make more sense. You can imagine your Rails project as being a giant container where all of your logic lives. In order to have structure, you create different child containers where different items may live. These containers are file directories and our paths to their child components are composed of container names combined using the scope resolution operator.
If you have some grouped logic for “Customer” then you may put your individual class files in a “customers” directory. Whether you use a Rails generator command or manually add the files, you will notice that the class name you declare will be prefixed with “Customer::”. For example an “Orders” class would be declared as “class Customer::Orders”, but why is this?
Rails convention dictates that we use certain file naming and structure so that it can correctly manage the routes.rb file and allow our actions to go through to the correct controllers. In order to do this, it creates a scope for each of our directories. Though we don’t see this wrapper created explicitly, it is there and because of this our classes are encapsulated in a logic based directory container.
Based on the file structure, you may have a multiple level nested file and this will be reflected in the class name. Each directory creates a new nest level where the new class name will include that directory name in order to create a clear path from the global project namespace to our resource. For an extremely nested file with four parent directories you would end up with:
It’s an interesting convention when you are first learning about the Ruby scope resolution operator, but makes more sense as you build more and more complex apps.
Scope Resolution Operator Anti-Patterns to Avoid
A last point I’d like to make with Ruby’s scope resolution operator is an anti-pattern to avoid. It is common to access a constant stored on a class using the scope resolution operator. Below I have created a basic class, Order, with a constant and a class method:
If I access the DEFAULT_USER_ID constant using the scope resolution operator I get:
We expected this, but what happens when we use the scope resolution operator to try to access the class method?
Using the operator to access the class method works, but this doesn’t mean that we should do it this way. The expected use of this operator is to access a specific class or a constant defined on the class, and when we use it to access class methods we make our code harder to understand.
It would be unclear for another developer to get argument errors because they didn’t pass in arguments to a class method accessed with “::” instead of “.”. Remember, the scope resolution operator is used to access objects like classes, modules and variables, not functionality like class methods.
Being explicit in communication is very important to avoid misunderstandings. This importance of explicitness translates directly to programming as we want to make sure that we are accessing the correct classes and variables, as well as doing it in a way that is clear and understandable to other developers. I hope that you now have a better understanding of how we use Ruby’s scope resolution operator to be explicit in how we access values in simple and complex scopes!