class ObservableCollection
This class facilitates the modification of collections in a distributed fashion. It adds the Observable functionality to collections types like Hash and Array. Initialize with the underlying collection you want to wrap.
It supports nested collections. Notifications of changes in lower levels in a data structure are bubbled upward by chained-together ObservableCollections, where each chain link is an observable-observer relationship.
The object wrapped by an ObservableCollection
can be accessed explicitly via subject
, but the point of this class is that you can treat an ObservableCollection
as you would a regular Array or Hash.
Constants
- DESTRUCTIVE
The use of one of these methods may result in the subject changing
Attributes
Public Class Methods
Factory method - takes in an Array or a Hash, and the observer. Options are passed to constructor (see above). An additional option is :func, which specifies the name of the observer’s update callback (defaults to :update as in the Observable module).
# File lib/observable_collection.rb, line 40 def self.create(subject, observer = nil, opts = {}) observable = subject if [Array, Hash].include? observable.class observable = ObservableCollection.new(subject, opts) args = [observer] args << opts[:func] if opts[:func] observable.add_observer(*args) if observer end observable end
Options:
lock_file - path to a file used for locking always_update_after - always call update after the collection is accessed, regardless of whether the methods are destructive (update is always called *before* the collection is accessed)
# File lib/observable_collection.rb, line 30 def initialize(subject, opts = {}) @subject = subject @lock_file = opts[:lock_file] @always_update_after = opts[:always_update_after] end
Public Instance Methods
Gain an exclusive lock on access to this data structure. Accepts a block to execute while the lock is owned. It is best to do this whenever, e.g., writing to disk upon changes to the collection.
# File lib/observable_collection.rb, line 54 def lock _lock # (TL;DR: locking solves more problems than concurrency) # Only read from disk once while the lock is kept. Normal behavior is # to read every time a method is called at any level of the data # structure, which can cause problems when e.g. reading twice in one # line, such as `obs_hash[a][b] << obs_hash[c][d].count`. Note that the # problem being solved here is not related to concurrency--it's just # a convenient way to solve it. changed notify_observers(self, :before) yield _unlock end
Users will treat ObservableCollection
like a regular collection, so send method calls to the underlying collection. Extra things we do:
-catch the creation/retrieval of child collections and make them observable too, so that updates to them bubble up. -let *our* observers know about this method call, both before and after we call the desired method.
# File lib/observable_collection.rb, line 79 def method_missing(meth, *args, &block) # Let the our observers know someone is calling a method on us. If we are # reporting directly to a user-land observer, its callback will be # invoked. If we are reporting to another ObservableCollection, it will # just propagate the notification upward. unless @locked changed notify_observers(@subject, :before) end # Execute the method on the subject result = @subject.send(meth, *args, &block) # If the return value is another ObservableCollection, add myself as an # observer. If it's an ordinary collection, make it an ObservableCollection # and add myself as an observer. The exception is when result == @subject, # in which case we just want to return the subject unadorned. This is to # avoid, e.g., puts being unable to convert an observable array to a regular # array the way it expects (this exception facilitates, e.g., `puts hash.values`) if result.is_a? ObservableCollection result.add_observer self elsif result != @subject result = ObservableCollection.create(result, self, lock_file: @lock_file, always_update_after: @always_update_after) end if (DESTRUCTIVE.include? meth) || @always_update_after changed notify_observers(@subject, :after) end result end
We ignore the arguments because we don’t care what the change was downstream–we just need to propagate upward the message that something changed.
# File lib/observable_collection.rb, line 118 def update(_downstream_object, kind) if kind == :after changed notify_observers(@subject, :after) end end
Protected Instance Methods
# File lib/observable_collection.rb, line 127 def _lock unless @lock_file throw "Can't use lock feature without specifying a lock file" end @lock = File.open(@lock_file, 'a+') @lock.flock(File::LOCK_EX) @locked = true end
# File lib/observable_collection.rb, line 136 def _unlock @lock.close @locked = false end