Dependency 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dependency.rb ADDED
@@ -0,0 +1,141 @@
1
+ # Just a namespace.
2
+ module Dependency
3
+ # This is the module you need to include in your classes (or Object).
4
+ module Methods
5
+ # Dependents can be any type of object, but must
6
+ # have the <tt>update</tt> method defined as follows:
7
+ #
8
+ # def update(aspect)
9
+ # end
10
+ #
11
+ # or:
12
+ #
13
+ # def update(aspect, message)
14
+ # # message[:from] always defined
15
+ # # message[:with] usually defined
16
+ # end
17
+ #
18
+ # Adding a dependent:
19
+ #
20
+ # object.dependents.add dependent
21
+ # object.dependents << dependent
22
+ #
23
+ # Removing a dependent:
24
+ #
25
+ # object.dependents.remove dependent
26
+ #
27
+ def dependents
28
+ @dependents ||= Proxy.new
29
+ end
30
+ # Call this method to notify of a change, e.g.
31
+ #
32
+ # def name=(new_name)
33
+ # old_name = self.name
34
+ # self.name = new_name
35
+ # changed :name, :with => old_name
36
+ # end
37
+ #
38
+ def changed(aspect = nil, message = nil)
39
+ args = [:update, aspect]
40
+ unless message.nil?
41
+ message[:from] = self
42
+ args << message
43
+ end
44
+ flexi = FlexiCaller.new(*args)
45
+ dependents.each { |dependent| flexi.call_on dependent }
46
+ end
47
+ # Register a dependent that's only interested in a particular aspect.
48
+ #
49
+ # object.express_interest_in :name, :for => dependent
50
+ #
51
+ # Use the :send_back option to specify what message (method call) should
52
+ # be sent back (defaults to :update).
53
+ #
54
+ # object.express_interest_in :name, :for => dependent, :send_back => :name_changed
55
+ #
56
+ # Note: the method called on the dependents has a different (simpler!)
57
+ # signature to that when just using `dependents.add`:
58
+ #
59
+ # def update(old_name)
60
+ # def update(old_name, object)
61
+ #
62
+ def express_interest_in(aspect, options)
63
+ dependents.add Transformer.new(self, aspect, options[:for], options[:send_back] || :update)
64
+ end
65
+ # Remove a dependency:
66
+ #
67
+ # object.retract_interest_in :name, :for => dependent
68
+ #
69
+ def retract_interest_in(aspect, options)
70
+ dependents.delete_if do |dependent|
71
+ dependent.respond_to?(:for?) && dependent.for?(aspect, options[:for])
72
+ end
73
+ end
74
+ end
75
+
76
+ # Include this module in your classes (or Object),
77
+ # if you want to use the dependency_attr class method.
78
+ module ClassMethod
79
+ # Shorthand for the following pattern:
80
+ #
81
+ # def name=(new_name)
82
+ # old_name = self.name
83
+ # self.name = new_name
84
+ # changed :name, :with => old_name
85
+ # end
86
+ #
87
+ # Use it as you would `attr`, e.g.
88
+ #
89
+ # dependency_attr :name, :age
90
+ #
91
+ def dependency_attr(*names)
92
+ for attribute in names
93
+ attr_reader attribute
94
+ class_eval %{
95
+ def #{attribute}=(new_value)
96
+ old_value = self.#{attribute}
97
+ instance_variable_set '@#{attribute}', new_value
98
+ changed :#{attribute}, :with => old_value
99
+ end
100
+ }
101
+ end
102
+ end
103
+ end
104
+
105
+ private # keep oot!
106
+
107
+ class Proxy < Array # :nodoc:
108
+ def remove(dependent)
109
+ delete_if { |d| d === dependent }
110
+ end
111
+ alias :add :<<
112
+ end
113
+
114
+ class FlexiCaller # :nodoc:
115
+ def initialize(method_name, *args)
116
+ @method_name, @args = method_name, args
117
+ end
118
+ def call_on(dependent)
119
+ case arity = dependent.method(@method_name).arity
120
+ when 0
121
+ dependent.send @method_name
122
+ when -1
123
+ dependent.send @method_name, *@args
124
+ else
125
+ dependent.send @method_name, *@args[0, arity]
126
+ end
127
+ end
128
+ end
129
+
130
+ class Transformer # :nodoc:
131
+ def initialize(parent, aspect, dependent, method_name)
132
+ @parent, @aspect, @dependent, @method_name = parent, aspect, dependent, method_name
133
+ end
134
+ def update(aspect = nil, message = {})
135
+ FlexiCaller.new(@method_name, message[:with], @parent).call_on(@dependent) if @aspect == aspect
136
+ end
137
+ def for?(aspect, dependent)
138
+ @aspect == aspect && @dependent.equal?(dependent)
139
+ end
140
+ end
141
+ end
data/rakefile.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/packagetask'
5
+ require 'rake/gempackagetask'
6
+ require 'rcov/rcovtask'
7
+ require 'rake/rdoctask'
8
+
9
+ module Dependency
10
+ VERSION = '1.0.0'
11
+ PACK = ['readme.txt', 'rakefile.rb', '{lib,spec}/*.rb']
12
+ SPEC = ['spec/*_spec.rb']
13
+ RDOC = ['readme.txt', 'lib/*.rb']
14
+ end
15
+
16
+ gemspec = Gem::Specification.new do |s|
17
+ s.name = 'Dependency'
18
+ s.summary ='Smalltalk-inspired dependency mechanism.'
19
+ s.version = Dependency::VERSION
20
+ s.files = FileList[*Dependency::PACK]
21
+ s.require_path = 'lib'
22
+ s.autorequire = 'dependency'
23
+ s.has_rdoc = true
24
+ s.extra_rdoc_files << Dependency::RDOC.first
25
+ s.rdoc_options << '--main' << Dependency::RDOC.first
26
+ s.rubyforge_project = s.name.downcase
27
+ s.homepage = "http://#{s.rubyforge_project}.rubyforge.org/"
28
+ s.author = 'Tim Fletcher'
29
+ s.email = 'twoggle@gmail.com'
30
+ end
31
+
32
+ Rake::GemPackageTask.new(gemspec) { |t| t.package_dir = 'pkg' }
33
+
34
+ Rake::PackageTask.new(gemspec.name, Dependency::VERSION) do |p|
35
+ p.need_tar_gz = true
36
+ p.package_files.include *Dependency::PACK
37
+ end
38
+
39
+ Rake::TestTask.new :spec do |t|
40
+ t.test_files = Dependency::SPEC
41
+ end
42
+
43
+ Rcov::RcovTask.new do |t|
44
+ t.test_files = Dependency::SPEC
45
+ t.output_dir = 'coverage'
46
+ t.rcov_opts = []
47
+ end
48
+
49
+ Rake::RDocTask.new do |t|
50
+ t.main = Dependency::RDOC.first
51
+ t.rdoc_files.include *Dependency::RDOC
52
+ end
data/readme.txt ADDED
@@ -0,0 +1,39 @@
1
+ = Dependency
2
+
3
+ Smalltalk-inspired dependency mechanism.
4
+
5
+
6
+ == Basic HOWTO
7
+
8
+ In your classes (or Object):
9
+
10
+ include Dependency::Methods
11
+
12
+ Register dependencies:
13
+
14
+ object.dependents.add dependent
15
+
16
+ Notify of changes like so:
17
+
18
+ changed :attr_name, :with => old_value
19
+
20
+ The update method will then be called on all the object's dependents.
21
+
22
+ Remove dependencies:
23
+
24
+ object.dependents.remove dependent
25
+
26
+ For more insight, take a look at the source, the spec, or Chapter 19 of
27
+ Smalltalk by Example, upon which this code was based.
28
+
29
+ Smalltalk by Example: http://www.iam.unibe.ch/~ducasse/FreeBooks/ByExample
30
+
31
+
32
+ == License
33
+
34
+ Same as Ruby.
35
+
36
+
37
+ == Contact
38
+
39
+ Tim Fletcher <mailto:twoggle@gmail.com>
@@ -0,0 +1,48 @@
1
+ #
2
+ # Needs [test/spec](http://chneukirchen.org/repos/testspec)
3
+
4
+ $:.unshift File.dirname(__FILE__)
5
+
6
+ require 'test/spec'
7
+ require 'preamble'
8
+
9
+ $stock = Stock.new('DEPCO')
10
+ $simple_watcher = SimpleStockWatcher.new($stock)
11
+ $selective_watcher = SelectiveStockWatcher.new($stock)
12
+ $selective_watcher_with_send_back = SelectiveStockWatcherWithSendBack.new($stock)
13
+
14
+ $stock.traded 200, :price => 20
15
+ $stock.traded 100, :price => 30
16
+ $stock.name = 'NEWCO'
17
+ $stock.dependents.remove $simple_watcher
18
+ $stock.retract_interest_in :price, :for => $selective_watcher
19
+ $stock.retract_interest_in :price, :for => $selective_watcher_with_send_back
20
+ $stock.traded 150, :price => 25
21
+
22
+
23
+ context 'Simple Stock Watcher' do
24
+ specify 'should receive 3 calls to #update' do
25
+ $simple_watcher.method_calls.should.equal :update => 3
26
+ end
27
+ specify 'should receive correct price changes' do
28
+ $simple_watcher.price_changes.should.equal [[0, 20], [20, 30]]
29
+ end
30
+ end
31
+
32
+ context 'Selective Stock Watcher' do
33
+ specify 'should receive 2 calls to #update' do
34
+ $selective_watcher.method_calls.should.equal :update => 2
35
+ end
36
+ specify 'should receive correct price changes' do
37
+ $selective_watcher.price_changes.should.equal [[0, 20], [20, 30]]
38
+ end
39
+ end
40
+
41
+ context 'Selective Stock Watcher w/ Send Back' do
42
+ specify 'should receive 2 calls to #price_changed' do
43
+ $selective_watcher_with_send_back.method_calls.should.equal :price_changed => 2
44
+ end
45
+ specify 'should receive correct price changes' do
46
+ $selective_watcher.price_changes.should.equal [[0, 20], [20, 30]]
47
+ end
48
+ end
data/spec/preamble.rb ADDED
@@ -0,0 +1,53 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'dependency')
2
+
3
+ Object.send :include, Dependency::Methods
4
+ Class.send :include, Dependency::ClassMethod
5
+
6
+ class Stock
7
+ dependency_attr :price, :name
8
+ def initialize(name, price = 0)
9
+ @name, @price = name, price
10
+ end
11
+ def traded(count, args)
12
+ self.price = args[:price]
13
+ end
14
+ end
15
+
16
+ class StockWatcher
17
+ def method_calls
18
+ @method_calls ||= Hash.new { |calls, meth| calls[meth] = 0 }
19
+ end
20
+ def price_changes
21
+ @price_changes ||= []
22
+ end
23
+ end
24
+
25
+ class SimpleStockWatcher < StockWatcher
26
+ def initialize(stock)
27
+ stock.dependents.add self
28
+ end
29
+ def update(aspect, message)
30
+ method_calls[:update] += 1
31
+ price_changes << [message[:with], message[:from].price] if :price == aspect
32
+ end
33
+ end
34
+
35
+ class SelectiveStockWatcher < StockWatcher
36
+ def initialize(stock)
37
+ stock.express_interest_in :price, :for => self
38
+ end
39
+ def update(old_price, stock)
40
+ method_calls[:update] += 1
41
+ price_changes << [old_price, stock.price]
42
+ end
43
+ end
44
+
45
+ class SelectiveStockWatcherWithSendBack < StockWatcher
46
+ def initialize(stock)
47
+ stock.express_interest_in :price, :for => self, :send_back => :price_changed
48
+ end
49
+ def price_changed(old_price, stock)
50
+ method_calls[:price_changed] += 1
51
+ price_changes << [old_price, stock.price]
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: Dependency
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2006-10-20 00:00:00 +01:00
8
+ summary: Smalltalk-inspired dependency mechanism.
9
+ require_paths:
10
+ - lib
11
+ email: twoggle@gmail.com
12
+ homepage: http://dependency.rubyforge.org/
13
+ rubyforge_project: dependency
14
+ description:
15
+ autorequire: dependency
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Tim Fletcher
31
+ files:
32
+ - readme.txt
33
+ - rakefile.rb
34
+ - lib/dependency.rb
35
+ - spec/dependency_spec.rb
36
+ - spec/preamble.rb
37
+ test_files: []
38
+
39
+ rdoc_options:
40
+ - --main
41
+ - readme.txt
42
+ extra_rdoc_files:
43
+ - readme.txt
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ requirements: []
49
+
50
+ dependencies: []
51
+