The Barewords Pattern allows you message plain bare word methods with no additional ceremony so you can use the same syntax throughout your object. Most importantly, this pattern keeps your objects consistent by improving searchability, debugability, and refactoring. Consider this simple example:
class Name
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
Notice, by using attr_reader, we have a clean way to assemble the full name (i.e. first, middle, and last) when casting to a string using bare words without littering your implementation with @ symbols for instance variables. Instead, everything is clean and consistent.
Now that you understand the basics, let’s dive deeper.
Guidelines
There are a few guidelines to keep in mind when using this pattern such as:
-
No surrounding quotes.
-
No special sigil as a prefix (i.e.
$,@,@@, etc). -
No preceding method call syntax.
-
No ending parenthesis.
-
No uppercase (i.e.
EXAMPLE), snakecase (i.e.ExAmPle), or any character that would not be used in a typical Ruby method.
Usage
Returning to the Name object, shown earlier, let’s dissect how the implementation adheres to the Barewords Pattern:
-
The
@first,@middle, and@lastinstance variables are initialized via the constructor and never referenced again. Construction should be the only place instance variables are used. -
The public API of the
Nameobject is locked down using theprivate attr_readermacro to ensure@first,@middle, and@lastare bareword methods and are read-only to prevent mutability. -
The
#to_sinstance method is scoped to only using the bareword methods without having to know the specifics of using globals, instance variables, constants, etc.
For example, here is the proper use of a bareword:
example
On the flip side, the following should be avoided with caveats to be pointed out shortly:
$example # Global variable
EXAMPLE # Constant variable
@@example # Class variable
@example # Instance variable
In fact, global and class variables should be avoided in general, not when only using barewords. They are a code smell and lead to hard to maintain code due to their broad and far reaching scopes. For example, here is a global variable example that should be avoided:
$middle = "Xavier"
class Name
def initialize first, last
@first = first
@last = last
end
def to_s = "#{first} #{$middle} #{last}"
private
attr_reader :first, :last
end
The above is a code smell due to the following reasons:
-
Introduces a Global Variable Antipattern.
-
Use of
$middlemeans the global variable can be mutated and referenced anywhere in the entire program. -
Having a hard coded reference to the
$middleglobal variable means specifically searching for$middleinstead ofmiddlewhen refactoring. -
Breaks encapsulation because
$middleis not scoped to the nameNameobject.
Same goes for class variables:
class Name
@@middle = "Xavier"
def initialize first, last
@first = first
@last = last
end
def to_s = "#{first} #{@@middle} #{last}"
private
attr_reader :first, :last
end
Unlike the $middle global variable example, @@middle is scoped to Name. Unfortunately, if we
subclassed Name and mutated @@middle, both Name (superclass) and the corresponding subclass
would be updated to share the same value. Again, do yourself a favor and avoid the
Class Variable Antipattern.
Constants, on the other hand, are acceptable as long as they are scoped to a module and/or class. Even then, though, constants should be injected into the object being initialized for maximum benefit. For example, avoid the following:
class Name
MIDDLE = "Xavier"
def initialize first, last
@first = first
@last = last
end
def to_s = "#{first} #{MIDDLE} #{last}"
private
attr_reader :first, :last
end
While the above limits the scope of MIDDLE to the Name object, use of the constant still breaks
encapsulation as first shown with the $middle global variable example earlier. Here’s a better
solution:
class Name
MIDDLE = "Xavier"
def initialize first, last, middle: MIDDLE
@first = first
@last = last
@middle = middle
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
The above is good because it accomplishes the following:
-
Leverages
MIDDLEas a clearly defined default constant. -
Initializes
Namewithmiddlebeing a keyword argument defaulting to theMIDDLEconstant as the value. This also allows themiddlekeyword argument to be customized should someone need to construct the object with default value other thanMIDDLE. -
Allows
middleto be a bareword viaattr_readerso the implementation remains locally scoped and properly encapsulated.
Finally, instance variables are the most common use case. What you want to avoid is the following:
class Name
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{@first} #{@middle} #{@last}"
end
With the above, the object’s API access to @first, @middle, and @last remains private.
Unfortunately, we are not using barewords in the #to_s method. This brings us back to the
screenshot at the start of this article. What we want is the following:
class Name
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
Granted, the above adds a few more lines of code, which some people might grumble about, but the payoff for flexible and maintainable code is worth the effort.
Advantages
There are several advantages to the Barewords Pattern, which are:
-
The ability to allow greater flexibility when enhancing or refactoring code by limiting scope and reducing complexity. For example, when discussing the earlier
Nameobject, we talked about this object not having to concern itself with where the values come from, how they should be interpolated, an so forth.Name, instead, has only the sole responsibility of concatenating a string. By using a consistent syntax, we have a single way to send the messages we want. -
An immediate exception when making a typo. Instead of using
firstor@firstand you usefrstinstead, you’ll get an exception stating that no local variable exists:undefined local variable or method `frst'. Whereas, had you used@frstinstead of@first, you’ll end up with anilwhich is much harder to debug. Having an exception thrown when a typo is introduced provides immediate feedback for quickly fixing the problem before deploying code to production and having to track down where a silentnilwas introduced into the code. -
Takes less key strokes and finger traversal to type
firstinstead of@first.
Disadvanges
A slight disadvantage is the extra lines of code — the private attr_reader — required to
generate private methods for the instance variables.
Performance
In terms of performance, there is no notable difference. Use the following script to see for yourself:
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "benchmark-ips"
gem "debug"
end
class Default
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{@first} #{@middle} #{@last}"
end
class Bare
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
default = Default.new "Zoe", "Alleyne", "Washburne"
bare = Bare.new "Zoe", "Alleyne", "Washburne"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("With") { bare.to_s }
benchmark.report("Without") { default.to_s }
benchmark.compare!
end
__END__
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24.2.0]
Warming up --------------------------------------
With 1.248M i/100ms
Without 1.249M i/100ms
Calculating -------------------------------------
With 13.436M (± 1.3%) i/s (74.43 ns/i) - 67.385M in 5.016010s
Without 13.424M (± 0.9%) i/s (74.49 ns/i) - 67.429M in 5.023216s
Comparison:
With: 13436271.6 i/s
Without: 13424476.6 i/s - same-ish: difference falls within error
As you can see, the performance hit is negligible.
Sublime Text
Due to this pattern being is so useful, and assuming you use Sublime Text, you can use a Sublime Text Setup snippet to quickly craft new objects. For example, to execute the snippet, you could type initb, which is short for initialize body, to yield the following template:
def initialize $1 $2 end private attr_reader :$3
The template’s $1, $2, and $3 tab stops allow you to quickly fill in the rest of your implementation while still adhering to the Barewords Pattern.
Initable
To reduce typing or avoid the Sublime Text snippet altogether, you can supercharge your Ruby code further by using the Initable gem. Initable is designed to adhere to the Barewords Pattern while allowing you to write a single line of code. Here’s a before and after for comparison:
Before
class Name
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
Name.new "Zoe", "Alleyne", "Washburne"
#<Name:0x0000000128760d80 @first="Zoe", @middle="Alleyne", @last="Washburne">
After
class Name
include Initable[%i[req first], %i[req middle], %i[req last]]
end
Name.new "Zoe", "Alleyne", "Washburne"
#<Name:0x000000013273ec30 @first="Zoe", @middle="Alleyne", @last="Washburne">
Notice how, when using Initable, you can create a Name instance with the same values by only using one line of code! There’s a lot you can do with Initable, so check out the documentation to learn more.
Avoidances
There are a few solutions in this space that should be avoided:
-
Ivar: This gem performs static and dynamic checks for unused/misspelled instance variables which adds another dependency, a lot more syntax, and effort than if you were to stick with the default attribute readers/writers and keep your instance variables relegated to your initializer.
-
Strict Ivars: This is a pre-processor that will mutate your code upon boot by applying safety checks to your instance variables but this requires setup and constant maintenance that can lead to surprising debugging situations (especially when your code doesn’t match runtime behavior).
Conclusion
Now that you have a good understanding of the Barewords Pattern, you can refactor your code with confidence by making all of your objects consistent and a joy to read with improved maintainability.