Cushion Defaults
What Does It Do?
TL;DR
An easy, flexible, and powerful alternative to hashes of defaults. Can be used both in individual classes and in complex class hierarchies.
The Long Version
Allows you to specify a "cushion" for various instance variables—effectively a default value—that will be returned by optional reader methods if the instance variable is undefined.
If, for example, the default value for @wheels
in class Car is 4 and we set up a new instance called ford
(ford = Car.new
), then calling ford.wheels
will return 4—even though no instance variable @wheels
has been directly specified for ford
. And if we later change the default value of wheels
for Car (Car.defaults[:wheels] = 6
), then all subsequent calls to ford.wheels
will return 6 (unless we crystallize it beforehand—see below for more details).
The Latest
I try to keep all of this up-to-date, but the latest information can always be found in CHANGELOG.md.
Why Should I Care?
- Don't Repeat Yourself (DRY): Gather your defaults in one place, and specify them only once.
- Correspondingly, minimize the amount of code you have to write and maintain. You'll be writing
x || default_value_for_x
a lot less—and if you later change the default value for x, you only have to update a single line of code. - Easily allow subclasses to inherit the default values of their ancestor classes or override them with their own default values. CushionDefaults is in this respect more flexible than using either constants or
@@defaults
variables. As an added bonus, changes to the defaults of a superclass cascade down and affect the defaults of subclasses. - Optionally, if you think of your defaults as configuration rather than logic, pull them out of your code and put them in class-specific YAML files that can be automatically loaded in.
- Using the YAML technique, you can maintain multiple sets of defaults and load in the appropriate one depending on the environment.
- If you follow the common pattern of setting instance variables to the default value (e.g.,
@var = params[:var] || default_for_var
), you have no way of distinguishing between whenx@var
is set to the default value because that was actively selected (params[:var]...
) and when it is set to the default because it otherwise would have been nil (... || default_for_var
). This is especially important when defaults change occasionally (or even vary regularly!). CushionDefaults makes this situation easy to handle.
Give Me a Quick Example
require 'cushion_defaults' # This will be assumed henceforth.
require 'color'
class Person
include CushionDefaults
# Set the cushion or default value for @favorite_color to the static value 'blue'
self.defaults[:favorite_color] = 'blue'
# Set the cushion for @favorite_shade_of_gray to the following proc.
# As long as @favorite_shade_of_gray is not set, this proc will be evaluated at each call to #favorite_shade_of_gray.
# If we wanted to fix or crystallize the value, we could call the bang version #favorite_shade_of_gray!
self.defaults[:favorite_shade_of_gray] = proc do |instance|
Color::RGB.by_name(instance.favorite_color).to_grayscale.to_rgb
end
# #cushion sets up a cushion_reader and cushion_writer for each of the symbols passed in
cushion :favorite_color, :favorite_shade_of_gray
end
ryan, julia = Person.new, Person.new
ryan.favorite_color = 'blue'
# This value is computed by the proc cushion defined above, since ryan's @favorite_shade_of_gray isn't defined
ryan.favorite_shade_of_gray # RGB [#808080]
ryan.favorite_shade_of_gray = Color::RGB.by_name('silver') # RGB [#cccccc]
# Since ryan's @favorite_shade_of_gray is defined now, the proc isn't called
ryan.favorite_shade_of_gray # RGB [#cccccc]
ryan.favorite_color # 'blue'
ryan.has_specified?(:favorite_color) # true
julia.favorite_color # 'blue'
# When we call julia.favorite_color, we're only getting the default color
julia.has_specified?(:favorite_color) # false
# Now we set the default value for @favorite_color to a new value.
Person.defaults[:favorite_color] = 'green'
# ryan has a custom favorite color, so it doesn't affect him, but julia returns 'green' now
ryan.favorite_color # 'blue'
julia.favorite_color # 'green'
How Do I Get It?
gem install 'cushion_defaults'
if you just want the gem.
If you want to help out the project or edit the source code, clone the repository (hosted at GitHub), fork it, make changes, and make a pull request.
Give Me the Rundown
The Basics
Setting up a DefaultsHash, populating it, and setting up cushion_reader
s and cushion_writer
s is a simple process.
class Plant
include CushionDefaults
self.defaults = {color: 'green', sunlight_needed: 'full'}
# cushion_defaults is here equivalent to:
# cushion_reader :color, :sunlight_needed
# cushion_writer :color, :sunlight_needed
cushion_defaults
def needs_full_sunlight?
sunlight_needed.eql?('full')
end
end
rhododendron = Plant.new
rhododendron.color # 'green'
rhododendron.needs_full_sunlight? # true
Now, if we later decide to place our Plant class within Brandon Sanderson's Mistborn world, we may want to update our defaults:
Plant.defaults[:color] = 'brown'
As soon as we do this, all Plants that do not have a color explicitly assigned will return the new default value when we call their #color
method.
rhododendron.color # 'brown'
Defaults and Inheritance
Classes inherit the defaults of those ancestors that respond to #defaults
with a Hash or a descendent thereof.
This all takes place automatically. When CushionDefaults is included in a class, it automatically includes itself in all classes that subclass that class, and when a cushion_reader
is called, it automatically moves up the class hierarchy if no value for the key is specified in the instance variable or in the current class.
class Klass
include CushionDefaults
self.defaults = {first: Klass, second: Klass, third: Klass}
cushion_defaults
end
class SubKlass < Klass
self.defaults += {second: SubKlass, fourth: SubKlass}
cushion :fourth
end
class SubSubKlass < SubKlass
self.defaults[:third] = SubSubKlass
end
x, y, z = Klass.new, SubKlass.new, SubSubKlass.new
z.first = 'custom'
Calling #first
, #second
, #third
, and #fourth
, then, would produce the following results on x, y, and z:
[x.first, x.second, x.third]
# [Klass, Klass, Klass]
# x.fourth would return NoMethodError
[y.first, y.second, y.third, y.fourth]
# [Klass, SubKlass, Klass, SubKlass]
[z.first, z.second, z.third, z.fourth]
# ['custom', SubKlass, SubSubKlass, SubKlass]
Obviously, changing the default of a parent class changes the value returned by subclass instances, unless they have explicitly overridden the default.
SubKlass.defaults[:second] = 'totally new value'
z.class # SubSubKlass, which < SubKlass
z.second # 'totally new value'
Adding and Removing Readers and Writers
Now, if we were to later add a new default to our Plant class from up above
Plant.defaults[:climate] = 'temperate'
and ran
rhododendron = Plant.new
rhododendron.climate
we would get a NoMethodError
.
By default, CushionDefaults does not automatically add or remove readers and writers when defaults are added and removed. To change methods, you need to manually add readers and writers for the new default:
Plant.cushion :climate
If at any point you want to manually remove the cushion_reader
or cushion_writer
for a class (although the need for this should be rare, as you can simply overwrite it), you can run the following:
Plant.remove_reader :climate
Plant.remove_writer :climate
Alternatively, CushionDefaults can automatically add and remove methods for any new defaults added and any existing defaults removed. But to do that, we need to configure CushionDefaults.
Configuring CushionDefaults
There are two recommended techniques for configuring CushionDefaults (although a few other variations will work as well).
The simplest is to use CushionDefaults.configure
, which yields a CushionDefaults::Configuration
object that can be modified by a number of different methods, detailed in the docs.
CushionDefaults.configure do |conf|
conf.update_readers = true
conf.update_writers = true
end
If the above #configure
call is placed immediately after the require statement, then no explicit calls to cushion
, cushion_reader
, or cushion_writer
are needed.
CushionDefaults.configure do |conf|
conf.update_readers = true
conf.update_writers = true
end
class Chair
include CushionDefaults
self.defaults = {material: 'wood', comfort_factor: 5}
end
dining_room_hardback = Chair.new
dining_room_hardback.comfort_factor = 3
dining_room_hardback.material # 'wood'
dining_room_hardback.comfort_factor # 3
# automatically adds #number_accomodated and #number_accomodated=, because of above-specified options
Chair.defaults[:number_accomodated] = 1
dining_room_hardback.number_accomodated # 1
As an alternative to the CushionDefaults.configure
block, you can define a cushion_defaults.yaml file. By default, CushionDefaults looks for this at config/cushion_defaults.yaml
(relative either to the directory of the first file to require CushionDefaults or the gem's location in the file system). The YAML format is unremarkable, with the above CushionDefaults.config do ... end
block equivalent to:
update_readers: true
update_writers: true
For a complete list of options available (along with explanations), see the docs for CushionDefaults::Configuration, especially the method group "Option Reader/Writers."
Proc Cushions
CushionDefaults now supports proc cushions, which offer a powerful new level of flexibility in getting and setting defaults.
If a default is set to a Proc, then cushion_readers will yield both an instance variable and a symbol representing the instance variable queried. (Since it's a proc, though, we do not need to worry about everything passed in.)
Take the following example:
class Language
attr_accessor :say_hello
def initialize(&block)
yield self if block_given?
end
end
$languages = {
en: Language.new { |l| l.say_hello = 'Hello' },
fr: Language.new { |l| l.say_hello = 'Bonjour' }
}
class Person
include CushionDefaults
attr_accessor :name
def initialize(&block)
yield self if block_given?
end
self.defaults[:language] = $languages[:en]
# By default, return the greeting for the person's language and the person's name
self.defaults[:greeting] = proc do |instance|
"#{instance.language.say_hello}, #{instance.name}"
end
cushion_defaults
end
peter = Person.new { |p| p.name = 'Peter' }
peter.greeting # 'Hello, Peter'
pierre = Person.new { |p| p.name = 'Pierre' }
pierre.greeting # 'Hello, Pierre', since languages[:en] is the default language
pierre.language = $languages[:fr]
pierre.greeting # 'Bonjour, Pierre', since languages[:fr] is now pierre's language, and #greeting gets its #say_hello
pierre.greeting = 'Salut!'
pierre.greeting # 'Salut!', since pierre has a custom greeting
It is also possible to combine this technique with calls to writer methods to produce a lazily-evaluated instance variable.
class Person
include CushionDefaults
self.defaults[:when_i_noticed_you] = proc do |instance|
instance.when_i_noticed_you = Time.now
end
cushion :when_i_noticed_you
end
passerby = Person.new
# since @when_i_noticed_you is undefined, above proc sets it to Time.now
passerby.when_i_noticed_you # Time.now
# wait a sec
sleep(1.0)
passerby.when_i_noticed_you == Time.now # false—1 sec later
Alternatively, you can write a normal proc and call the variable's bang_reader
if you're worried the variable may not be set.
class Person
include CushionDefaults
self.defaults[:when_i_noticed_you] = proc { Time.now }
cushion :when_i_noticed_you
end
passerby = Person.new
# since @when_i_noticed_you is undefined the bang_reader sets it to Time.now
passerby.when_i_noticed_you! # Time.now
# wait a sec
sleep(1.0)
passerby.when_i_noticed_you == Time.now # false—1 sec later
These techniques can provide sophisticated means of both setting cushions or defaults while allowing customizable values for particular instances.
Freezing and Thawing Defaults
You may wish to prevent a default from further modification, either permanently or temporarily. This can prevent silly mistakes that are otherwise difficult to track down. CushionDefaults makes this possible via a freezing and thawing API. The key methods here are #freeze_default
and #thaw_default
.
class BuffaloNY
include CushionDefaults
self.defaults = {temperature: -10}
freeze_default :temperature
end
# Raises CushionDefaults::FrozenDefaultError (< RunTimeError)
BuffaloNY.defaults[:temperature] = 60
# Assuming we caught the above error...
BuffaloNY.defaults[:temperature] == -10 # true
# Come summer, we can thaw the default
BuffaloNY.thaw_default :temperature
#And we can reset it without error
BuffaloNY.defaults[:temperature] = 60
Frozen defaults can still have their values overridden by child classes.
class NaturalLog
include CushionDefaults
self.defaults = {base: Math::E}
freeze_default :base
end
class UnnaturalLog < NaturalLog; end
# Raises CushionDefaults::FrozenDefaultError
NaturalLog.defaults[:base] = 1i
# Works
UnnaturalLog.defaults[:base] = 1i
Note that frozen defaults can still have their values modified if those values are themselves mutable. To prevent this, we need to use #deep_freeze
—but this should be done with caution.
(In some situations, even this can can fail to "fully" freeze an object. Check out ice_nine for a fuller solution.)
Finally, to freeze or thaw all defaults en masse, the API makes available #freeze_defaults
and #thaw_defaults
.
Storing Class Defaults in YAML Files
By default, CushionDefaults checks for YAML files for each class but does not complain if no YAML files are found. (If you want it to complain, set config.whiny_yaml
to true.)
CushionDefaults looks for these YAML files at config/cushion_defaults/class_name.yaml
. For class Klass, then, it would expect a config file at config/cushion_defaults/klass.yaml
. Classes in a namespace are expected to have their YAML files in a folder named after their namespace, e.g. Modjewel::Klass in config/cushion_defaults/modjewel/klass.yaml
.
These YAML files are completely unremarkable in form. Note that all defaults should be specified at root (not in defaults:
), and currently only simple types are processed. For the above Chair class, we could place the defaults in a YAML class file like the following:
# config/cushion_defaults/chair.yaml
material: 'wood'
comfort_factor: 5
number_accomodated: 1
For an example of all of this in action, look at examples/example3/example3.rb
and its class default files in examples/example3/config/cushion_defaults/
.
You can specify a different YAML source folder relative to the calling directory (config/cushion_defaults/
by default) by setting config.yaml_source_folder
, or you can specify an absolute path to the YAML source folder by setting config.yaml_source_full_path
.
If you ever are bug-hunting and want to see where CushionDefaults expects a YAML file to be located, you can pass the class object to config.yaml_file_for(klass)
.
These YAML files are loaded automatically (unless config.auto_load_from_yaml
has been set to false). But if you ever want to (wipe and) reload the defaults for a class—or load for the first time if the above option is disabled—use the class method defaults_from_yaml
.
Managing Multiple Class Defaults
Multiple Sets of Class Defaults
You can use the above techniques to maintain different sets of class defaults for all of your classes. This is especially useful if your application needs to run in different environments or regions. For a more complex (but still simple enough) example, see Example 4 in the examples folder. Following is a trivial example.
Assume we have four YAML class defaults files as follows:
# config/cushion_defaults/spr/user.yaml
weather_judgment: 'how nice'
# config/cushion_defaults/sum/user.yaml
weather_judgment: 'is it hot in here or is it just me?'
# config/cushion_defaults/fal/user.yaml
weather_judgment: "if only it weren't for the leaves"
# config/cushion_defaults/win/user.yaml
weather_judgment: "baby it's cold outside"
Combine this with the following Ruby, and we can get different defaults depending on the current meteorological season.
class Season
# Obviously this is only valid for the Northern hemisphere
attr_accessor :months, :short_code
def initialize(&block)
yield(self) if block_given?
end
def include?(date)
months.include?(date.month)
end
def yaml_source_path
"config/cushion_defaults/#{short_code}/"
end
end
seasons = [
Season.new {|s| s.months=[3,4,5]; s.short_code='spr'},
Season.new {|s| s.months=[6,7,8]; s.short_code='sum'},
Season.new {|s| s.months=[9,10,11]; s.short_code='fal'},
Season.new {|s| s.months=[12,1,2]; s.short_code='win'}
]
current_season = seasons.select{|s| s.include?(Date.today)}.first
# The following will set the root directory for all class defaults, depending on the current season.
# Possible resulting config paths:
# - `config/cushion_defaults/spr/`
# - `config/cushion_defaults/sum/`
# - `config/cushion_defaults/fal/`
# - `config/cushion_defaults/win/`
CushionDefaults.configure do |conf|
conf.yaml_source_path = current_season.yaml_source_path
end
class User
# Automatically loads in defaults from the above-selected path
cushion :weather_judgment
end
# Returns one of the messages from the above YAML files, depending on the current meteorological season
User.new.weather_judgment
Multiple Defaults for a Single Class
Alternatively, if there is only a single class whose defaults you would like to load in one of several forms, you can do something like the following:
# select a random language
current_lang = ['en','fr','de'].sample
class Person
include CushionDefaults
defaults_from_yaml "#{self.to_s}_#{current_lang}"
cushion_defaults
end
In this example, we load (randomly) either person_en.yaml
, person_fr.yaml
, or person_de.yaml
. For a fuller example along these lines, see examples/example4/example4.rb
.
Crystallizing Defaults
You can prevent auto-updating of default values, if desired, by calling #crystallize_default
on those instances you don't want auto-updated. #crystallize_default(sym)
effectively says "If no value for @sym
is explicitly set, then explictly set it to the default value." Obviously, then, #crystallize_default(sym)
affects only those instances that do not have a value explicitly specified for sym
.
tulip, rose = Plant.new, Plant.new
tulip.color # 'brown'
rose.color = 'red'
tulip.has_specified?(:color) # false
rose.has_specified?(:color) # true
# crystallizes :color to 'brown'
tulip.crystallize_default(:color)
# has no effect, since :color is already set to 'red'
rose.crystallize_default(:color)
tulip.has_specified?(:color) # true
Plant.defaults[:color] = 'green'
tulip.color # 'brown'
rose.color # 'red'
Plant.new.color # 'green'
(Crystallizing defaults should be carefully distinguished from freezing defaults: crystallizing defaults applies to specific instances, whereas freezing defaults applies to the class itself.)
What About Persistence?
The key rule when using CushionDefaults with any sort of persistence is this: use instance variables, and not cushion_reader
s, when preparing objects for storage.
If you use cushion_reader
s when storing your objects, you run the risk of accidentally crystallizing your defaults. Take, for instance, the following (incorrect) code:
class AccidentallyInLove
include CushionDefaults
self.defaults[:girl_for_me] = proc { %w(Sally Jane Dora Annabel).sample }
cushion :girl_for_me
def marshal_dump
# This is the problem: it returns the default for marshaling!
[girl_for_me]
end
def marshal_load array
self.girl_for_me = array.first
end
end
marco = AccidentallyInLove.new
# only 0.4% chance these are the same!
5.times { puts marco.girl_for_me }
marco = Marshal.load(Marshal.dump(marco))
# Marco settled down without realizing it
5.times { puts marco.girl_for_me }
Assuming your intent in marshaling isn't to crystallize the default and force him to settle down, you can either leave in place the default methods (which work) or overload marshal_dump
as follows:
def marshal_dump
[@girl_for_me]
end
(Note, however, that any sort of reconstruction of objects is incompatible with setting Configuration.ignore_attempts_to_set_nil
to false.)
Pushy and Polite Defaults
Pushy and polite defaults are an experimental feature. Rough documentation can be found in the docs, and more details will be forthcoming.
Testing and Bug Fixing
The most common testing configuration options are available by calling config.testing!
.
You may find the following methods helpful in testing and bug fixing:
instance#has_specified?(sym)
: returns true if the instance has the instance variable denoted bysym
defineddefaults#ish_keys
: returns the keys of both its defaults and those it inherits from its parentsdefaults#has_ish_key?(key)
: returns true ifkey
is an ish_key.defaults#where_is_that_default_again(sym)
: returns the closest ancestor class in whichsym
is defined as a default. If no ancestor class has it defined as a default, returnsnil
.
But I need...
If you need more than CushionDefaults offers right now, you've got a couple different options:
- Suggest a feature. Please explain why you think this feature would be valuable, and offer a couple different use cases to showcase how it would help people.
- Code a feature. Fork and pull, and I'll fold it in and implement it if it looks solid and generally useful.
- Check out Cascading Configuration. You may want to check out Cascading Configuration, which tackles a similar problem but offers a different approach and featureset.
Well, If You Ask Me...
Any feedback is very much appreciated!
For the Entomologists
Run into any bugs or issues? Please report them on the GitHub issue tracker.
Like It?
- Tell a friend.
- Star or fork the GitHub repository.
- If you're feeling generous, offer a tip.
Not Sold?
If you have the time, tell me why.
Not a useful concept? Don't like the implementation? Think the default configuration options should be different? Edit the wiki and let me know.
If you have any specific suggestions for how to make CushionDefaults better, I'd love to hear them.
Should I Stay or Should I Go Now?
Ultimately that's your decision. Try it, and see if it works for your use case. That said, here are some general guidelines from my perspective.
When Should I Use CushionDefaults?
When you need or want...
- DRY defaults handling.
- Flexibility.
- A powerful feature set for handling (and overriding) inheritance of defaults.
- To know when an instance variable is set to the default and when it is simply not specified.
- The ability to manage and maintain multiple sets of defaults (while remaining DRY).
- To separate your defaults (configuration) off from your codebase (logic).
- To gracefully handle changing defaults.
- To ensure that some or all of your defaults don't change.
When Shouldn't I Use CushionDefaults?
- When speed is absolutely critical. CushionDefaults is very fast (see
benchmarks/simple_benchmark.rb
), since it runs almost entirely on a series of hash lookups and adjusts methods on the fly depending on the current defaults setting. But there's no way it could consistently be as fast asattr_accessor
: it just does more, and more computations means more time. Current benchmarks show execution speeds of 1.1 to 1.6 times those ofattr_reader
in Ruby 2.1.5, with a smaller factor in Ruby 1.9.3—but this is a difference of 0.01s to 0.05s for 100,000 calls to the reader methods. If you have hundreds of thousands of calculations that you need performed lightning fast, you should look elsewhere; but otherwise, CushionReader should be fast enough for your purposes. - When working in a Rails environment. CushionDefaults may eventually spawn a companion project CushionDefaults-Rails, but for now it's just not the right tool for a Rails job. There are plenty of libraries that would be better for this purpose, e.g., default_value_for.
- When you want to keep your dependencies down. Some people end up with 150 apps on their phones; others end up with 150 gems in their projects. CushionDefaults itself doesn't depend on any other gems (in production), but that still doesn't mean it's worth the extra overhead to use it in every project.