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
defined -
defaults#ish_keys
: returns the keys of both its defaults and those it inherits from its parents -
defaults#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 (seebenchmarks/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.