Module: FeatureEnvy::LazyAccessor
- Defined in:
- lib/feature_envy/lazy_accessor.rb
Overview
Lazy accessors.
### Definition
A lazy attribute is an attribute whose value is determined on first access. The same value is used on all subsequent access without running the code to determine its value again.
Lazy attributes are impossible in Ruby (see the discussion below), but lazy accessors are, and are provided by this module.
### Applications
Deferring expensive computations until needed and ensuring they’re performed at most once.
### Usage
-
Enable the feature in a specific class via ‘extend FeatureEnvy::LazyAccessor` or …
-
Enable the feature in a specific scope (given module and all modules and classes contained within) using a refinement via ‘using FeatureEnvy::LazyAccessor`.
-
Define one or more lazy attributes via ‘lazy(:name) { definition }`.
-
Do NOT read or write to the underlying attribute, e.g. ‘@name`; always use the accessor method.
-
Lazy accessors are thread-safe: the definition block will be called at most once; if two threads call the accessor for the first time one will win the race to run the block and the other one will wait and reuse the value produced by the first.
-
It’s impossible to reopen a class and add new lazy accessors after **the any class using lazy accessors has been instantiated**. Doing so would either make the code thread-unsafe or require additional thread-safety measures, potentially reducing performance.
### Discussion
Ruby attributes start with ‘@` and assume the default value of `nil` if not assigned explicitly. Real lazy attributes are therefore impossible to implement in Ruby. Fortunately accessors (i.e. methods used to obtain attribute values) are conceptually close to attributes and can be made lazy.
A naive approach found in many Ruby code bases looks like this:
“‘ruby def highest_score_user
@highest_score_user ||= find_highest_score_user
end “‘
It’s simple but suffers from a serious flaw: if ‘nil` is assigned to the attribute then subsequent access will result in another attempt to determine the attribute’s value.
The proper approach is much more verbose:
“‘ruby def highest_score_user
# If the underlying attribute is defined then return it no matter its value.
return @highest_score_user if defined?(@highest_score_user)
@highest_score_user = find_highest_score_user
end “‘
### Implementation Notes
-
Defining a lazy accessor defines a method with that name. The corresponding attribute is not set before the accessor is called for the first time.
-
The first time a lazy accessor is added to a class a special module is included into it. It provides an ‘initialize` method that sets `@lazy_attributes_mutexes` - a hash of mutexes protecting each lazy accessor.
Defined Under Namespace
Classes: Error
Class Attribute Summary collapse
-
.mutex_factory ⇒ Object
readonly
A mutex factory used by the lazy accessors feature.
Class Method Summary collapse
-
.define(klass, name, &definition) ⇒ Object
Defines a lazy accessor.
Instance Method Summary collapse
-
#lazy(name, &definition) ⇒ Array<Symbol>
Defines a lazy accessor.
Class Attribute Details
.mutex_factory ⇒ Object (readonly)
A mutex factory used by the lazy accessors feature.
181 182 183 |
# File 'lib/feature_envy/lazy_accessor.rb', line 181 def mutex_factory @mutex_factory end |
Class Method Details
.define(klass, name, &definition) ⇒ Object
Defines a lazy accessor.
Required to share the code between the extension and refinement.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/feature_envy/lazy_accessor.rb', line 188 def define klass, name, &definition name = name.to_sym variable_name = :"@#{name}" mutex_name = LazyAccessor.mutex_factory.register klass, name klass.class_eval do # Include the lazy accessor initializer to ensure state related to # lazy accessors is initialized properly. There's no need to include # this module more than once. # # Question: is the inclusion check required? Brief testing indicates # it's not. if !include? Initialize include Initialize end [ define_method(name) do mutex = instance_variable_get(mutex_name) if mutex # rubocop:disable Style/SafeNavigation mutex.synchronize do if instance_variable_defined?(mutex_name) instance_variable_set variable_name, instance_eval(&definition) remove_instance_variable mutex_name end end end instance_variable_get variable_name end ] end end |
Instance Method Details
#lazy(name, &definition) ⇒ Array<Symbol>
Defines a lazy accessor.
The ‘definition` block will be called once when the accessor is used for the first time. Its value is returned and cached for subsequent accessor use.
120 121 122 |
# File 'lib/feature_envy/lazy_accessor.rb', line 120 def lazy name, &definition LazyAccessor.define self, name, &definition end |