
I first discovered the Barewords Pattern via Ruby Tapas - Episode 4 (Barewords) by Avdi Grimm many years back. Prior to learning of this pattern, I had used a combination of instance variables, methods, constants, etc. to implement my designs, which accomplished what I needed but made the code unmaintainable. Since then, I’ve found barewords are valuable because they simplify terminology and limit scope within scripts, objects, and/or methods. In this article, I’m sharing why the Barewords Pattern is valuable and how best to use it in your own code.
Advantages
There are several advantages to the Barewords Pattern, including:
-
The ability to allow greater flexibility when enhancing or refactoring code by limiting scope and reducing complexity. For example, in the code snippet at the start of this article, the
Name
implementation doesn’t have to concern itself with where the values come from, how they should be interpolated, etc.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
first
or@first
and you usefrst
instead, you’ll get an exception stating that no local variable exists:undefined local variable or method `frst'
. Whereas, had you used@frst
instead of@first
, you’ll end up with anil
which 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 silentnil
was introduced into the code. -
Takes less key strokes and finger traversal to type
first
instead 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.
Guidelines
Barewords originated from Perl but is applicable in other languages, too. For instance, in this article, I’ll use Ruby to demonstrate the requirements of a bareword:
-
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 Ruby snippet, at the start of this article, let’s dissect how the code adheres to the Barewords Pattern:
-
The
@first
,@middle
, and@last
instance variables are initialized via the constructor and never referenced again. In truth, construction should be the only place instance variables are used. -
The public API of the
Name
object is locked down using theprivate attr_reader
macro to ensure@first
,@middle
, and@last
are bareword methods and are read-only to prevent mutability. -
The
#to_s
instance 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 pointed out later:
$example # Global variable
EXAMPLE # Constant variable
@@example # Class variable
@example # Instance variable
In fact, global and class variables should be avoided in general, not just when 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 use case 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
$middle
means the global variable can be mutated and referenced anywhere in the entire program. -
Having a hard coded reference to the
$middle
global variable means specifically searching for$middle
instead ofmiddle
when refactoring. -
Breaks encapsulation because
$middle
is not scoped to the nameName
object.
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 use case, @@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
DEFAULT_MIDDLE = "Xavier"
def initialize first, last, middle: DEFAULT_MIDDLE
@first = first
@last = last
@middle = middle
end
def to_s = "#{first} #{middle} #{last}"
private
attr_reader :first, :middle, :last
end
The above is successful because it accomplishes the following:
-
Leverages
DEFAULT_MIDDLE
as a clearly defined default constant. -
Initializes
Name
withmiddle
being a keyword argument defaulting to theDEFAULT_MIDDLE
constant as the value. This also allows themiddle
keyword argument to be customized should someone need to construct the object with default value other thanDEFAULT_MIDDLE
. -
Allows
middle
to be a bareword viaattr_reader
so the implementation remains locally scoped and properly encapsulated.
Finally, instance variables are the most common use case. What you want to avoid, and often used, 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 I’d say the payoff for flexible/maintainable code is worth the effort in the end.
Performance
In terms of performance, there is a tiny hit to performance since the instance variables are wrapped in accessor methods. Use the following script to see for yourself:
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `snippet.rb` and run as `ruby snippet.rb`
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "debug"
gem "benchmark-ips", require: "benchmark/ips"
end
class NameBasic
def initialize first, middle, last
@first = first
@middle = middle
@last = last
end
def to_s = "#{@first} #{@middle} #{@last}"
end
class NameBare
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
basic = NameBasic.new "Zoe", "Alleyne", "Washburne"
bare = NameBare.new "Zoe", "Alleyne", "Washburne"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report "With Barewords" do
bare.to_s
end
benchmark.report "Without Barewords" do
basic.to_s
end
benchmark.compare!
end
The output of the above script should look similar to these results:
Warming up -------------------------------------- With Barewords 518.316k i/100ms Without Barewords 580.698k i/100ms Calculating ------------------------------------- With Barewords 5.267M (± 2.5%) i/s - 26.434M in 5.021731s Without Barewords 5.749M (± 3.7%) i/s - 29.035M in 5.057604s Comparison: Without Barewords: 5749389.7 i/s With Barewords: 5267326.8 i/s - 1.09x (± 0.00) slower
As you can see, there is a performance hit but it’s small.
Tooling
The Barewords Pattern is so useful, I use a
Sublime
Text Snippet to quickly craft new objects. To execute a snippet, I type initb
which is short for
initialize body to yield the following template:
def initialize $1 $2 end private attr_reader :$3
I can then use the $1
, $2
, and $3
tab stops to quickly fill out the rest of the template with
my implementation while still adhering to the Barewords Pattern.
Conclusion
Hopefully, you have a better sense of the Barewords Pattern and how to use it in your own code. May your code be easier to read, maintain, and more enjoyable to work with!