├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── Gemfile ├── bin ├── setup └── console ├── Rakefile ├── BSDL ├── observer.gemspec ├── test └── test_observer.rb ├── COPYING ├── README.md └── lib └── observer.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | end 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "observer" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.4 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest ] 19 | exclude: 20 | - ruby: 2.4 21 | os: macos-latest 22 | - ruby: 2.5 23 | os: macos-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true # Run "bundle install", and cache the result automatically. 32 | - name: Run test 33 | run: bundle exec rake test 34 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /observer.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Implementation of the Observer object-oriented design pattern.} 17 | spec.description = spec.summary 18 | spec.homepage = "https://github.com/ruby/observer" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | # Specify which files should be added to the gem when it is released. 25 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 26 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 27 | `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 28 | end 29 | spec.bindir = "exe" 30 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 31 | spec.require_paths = ["lib"] 32 | end 33 | -------------------------------------------------------------------------------- /test/test_observer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'observer' 4 | 5 | class TestObserver < Test::Unit::TestCase 6 | class TestObservable 7 | include Observable 8 | 9 | def notify(*args) 10 | changed 11 | notify_observers(*args) 12 | end 13 | end 14 | 15 | class TestWatcher 16 | def initialize(observable) 17 | @notifications = [] 18 | observable.add_observer(self) 19 | end 20 | 21 | attr_reader :notifications 22 | 23 | def update(*args) 24 | @notifications << args 25 | end 26 | end 27 | 28 | def test_observers 29 | observable = TestObservable.new 30 | 31 | assert_equal(0, observable.count_observers) 32 | 33 | watcher1 = TestWatcher.new(observable) 34 | 35 | assert_equal(1, observable.count_observers) 36 | 37 | observable.notify("test", 123) 38 | 39 | watcher2 = TestWatcher.new(observable) 40 | 41 | assert_equal(2, observable.count_observers) 42 | 43 | observable.notify(42) 44 | 45 | assert_equal([["test", 123], [42]], watcher1.notifications) 46 | assert_equal([[42]], watcher2.notifications) 47 | 48 | observable.delete_observer(watcher1) 49 | 50 | assert_equal(1, observable.count_observers) 51 | 52 | observable.notify(:cats) 53 | 54 | assert_equal([["test", 123], [42]], watcher1.notifications) 55 | assert_equal([[42], [:cats]], watcher2.notifications) 56 | 57 | observable.delete_observers 58 | 59 | assert_equal(0, observable.count_observers) 60 | 61 | observable.notify("nope") 62 | 63 | assert_equal([["test", 123], [42]], watcher1.notifications) 64 | assert_equal([[42], [:cats]], watcher2.notifications) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Observer 2 | 3 | The Observer pattern (also known as publish/subscribe) provides a simple 4 | mechanism for one object to inform a set of interested third-party objects 5 | when its state changes. 6 | 7 | ## Mechanism 8 | 9 | The notifying class mixes in the +Observable+ 10 | module, which provides the methods for managing the associated observer 11 | objects. 12 | 13 | The observable object must: 14 | 15 | * assert that it has +#changed+ 16 | * call +#notify_observers+ 17 | 18 | An observer subscribes to updates using Observable#add_observer, which also 19 | specifies the method called via #notify_observers. The default method for 20 | notify_observers is #update. 21 | 22 | ## Installation 23 | 24 | Add this line to your application's Gemfile: 25 | 26 | ```ruby 27 | gem 'observer' 28 | ``` 29 | 30 | And then execute: 31 | 32 | $ bundle 33 | 34 | Or install it yourself as: 35 | 36 | $ gem install observer 37 | 38 | ## Usage 39 | 40 | The following example demonstrates this nicely. A +Ticker+, when run, 41 | continually receives the stock +Price+ for its @symbol. A +Warner+ 42 | is a general observer of the price, and two warners are demonstrated, a 43 | +WarnLow+ and a +WarnHigh+, which print a warning if the price is below or 44 | above their set limits, respectively. 45 | 46 | The +update+ callback allows the warners to run without being explicitly 47 | called. The system is set up with the +Ticker+ and several observers, and the 48 | observers do their duty without the top-level code having to interfere. 49 | 50 | Note that the contract between publisher and subscriber (observable and 51 | observer) is not declared or enforced. The +Ticker+ publishes a time and a 52 | price, and the warners receive that. But if you don't ensure that your 53 | contracts are correct, nothing else can warn you. 54 | 55 | ```ruby 56 | require "observer" 57 | 58 | class Ticker ### Periodically fetch a stock price. 59 | 60 | include Observable 61 | 62 | def initialize(symbol) 63 | @symbol = symbol 64 | end 65 | 66 | def run 67 | last_price = nil 68 | loop do 69 | price = Price.fetch(@symbol) 70 | print "Current price: #{price}\n" 71 | if price != last_price 72 | changed # notify observers 73 | last_price = price 74 | notify_observers(Time.now, price) 75 | end 76 | sleep 1 77 | end 78 | end 79 | end 80 | 81 | class Price ### A mock class to fetch a stock price (60 - 140). 82 | def self.fetch(symbol) 83 | 60 + rand(80) 84 | end 85 | end 86 | 87 | class Warner ### An abstract observer of Ticker objects. 88 | def initialize(ticker, limit) 89 | @limit = limit 90 | ticker.add_observer(self) 91 | end 92 | end 93 | 94 | class WarnLow < Warner 95 | def update(time, price) # callback for observer 96 | if price < @limit 97 | print "--- #{time.to_s}: Price below #@limit: #{price}\n" 98 | end 99 | end 100 | end 101 | 102 | class WarnHigh < Warner 103 | def update(time, price) # callback for observer 104 | if price > @limit 105 | print "+++ #{time.to_s}: Price above #@limit: #{price}\n" 106 | end 107 | end 108 | end 109 | 110 | ticker = Ticker.new("MSFT") 111 | WarnLow.new(ticker, 80) 112 | WarnHigh.new(ticker, 120) 113 | ticker.run 114 | ``` 115 | 116 | Produces: 117 | 118 | ``` 119 | Current price: 83 120 | Current price: 75 121 | --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75 122 | Current price: 90 123 | Current price: 134 124 | +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134 125 | Current price: 134 126 | Current price: 112 127 | Current price: 79 128 | --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79 129 | ``` 130 | 131 | ## Development 132 | 133 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 134 | 135 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 136 | 137 | ## Contributing 138 | 139 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/observer. 140 | -------------------------------------------------------------------------------- /lib/observer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Implementation of the _Observer_ object-oriented design pattern. The 4 | # following documentation is copied, with modifications, from "Programming 5 | # Ruby", by Hunt and Thomas; http://www.ruby-doc.org/docs/ProgrammingRuby/html/lib_patterns.html. 6 | # 7 | # See Observable for more info. 8 | 9 | # The Observer pattern (also known as publish/subscribe) provides a simple 10 | # mechanism for one object to inform a set of interested third-party objects 11 | # when its state changes. 12 | # 13 | # == Mechanism 14 | # 15 | # The notifying class mixes in the +Observable+ 16 | # module, which provides the methods for managing the associated observer 17 | # objects. 18 | # 19 | # The observable object must: 20 | # * assert that it has +#changed+ 21 | # * call +#notify_observers+ 22 | # 23 | # An observer subscribes to updates using Observable#add_observer, which also 24 | # specifies the method called via #notify_observers. The default method for 25 | # #notify_observers is #update. 26 | # 27 | # === Example 28 | # 29 | # The following example demonstrates this nicely. A +Ticker+, when run, 30 | # continually receives the stock +Price+ for its @symbol. A +Warner+ 31 | # is a general observer of the price, and two warners are demonstrated, a 32 | # +WarnLow+ and a +WarnHigh+, which print a warning if the price is below or 33 | # above their set limits, respectively. 34 | # 35 | # The +update+ callback allows the warners to run without being explicitly 36 | # called. The system is set up with the +Ticker+ and several observers, and the 37 | # observers do their duty without the top-level code having to interfere. 38 | # 39 | # Note that the contract between publisher and subscriber (observable and 40 | # observer) is not declared or enforced. The +Ticker+ publishes a time and a 41 | # price, and the warners receive that. But if you don't ensure that your 42 | # contracts are correct, nothing else can warn you. 43 | # 44 | # require "observer" 45 | # 46 | # class Ticker ### Periodically fetch a stock price. 47 | # include Observable 48 | # 49 | # def initialize(symbol) 50 | # @symbol = symbol 51 | # end 52 | # 53 | # def run 54 | # last_price = nil 55 | # loop do 56 | # price = Price.fetch(@symbol) 57 | # print "Current price: #{price}\n" 58 | # if price != last_price 59 | # changed # notify observers 60 | # last_price = price 61 | # notify_observers(Time.now, price) 62 | # end 63 | # sleep 1 64 | # end 65 | # end 66 | # end 67 | # 68 | # class Price ### A mock class to fetch a stock price (60 - 140). 69 | # def self.fetch(symbol) 70 | # 60 + rand(80) 71 | # end 72 | # end 73 | # 74 | # class Warner ### An abstract observer of Ticker objects. 75 | # def initialize(ticker, limit) 76 | # @limit = limit 77 | # ticker.add_observer(self) 78 | # end 79 | # end 80 | # 81 | # class WarnLow < Warner 82 | # def update(time, price) # callback for observer 83 | # if price < @limit 84 | # print "--- #{time.to_s}: Price below #@limit: #{price}\n" 85 | # end 86 | # end 87 | # end 88 | # 89 | # class WarnHigh < Warner 90 | # def update(time, price) # callback for observer 91 | # if price > @limit 92 | # print "+++ #{time.to_s}: Price above #@limit: #{price}\n" 93 | # end 94 | # end 95 | # end 96 | # 97 | # ticker = Ticker.new("MSFT") 98 | # WarnLow.new(ticker, 80) 99 | # WarnHigh.new(ticker, 120) 100 | # ticker.run 101 | # 102 | # Produces: 103 | # 104 | # Current price: 83 105 | # Current price: 75 106 | # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75 107 | # Current price: 90 108 | # Current price: 134 109 | # +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134 110 | # Current price: 134 111 | # Current price: 112 112 | # Current price: 79 113 | # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79 114 | # 115 | # === Usage with procs 116 | # 117 | # The +#notify_observers+ method can also be used with +proc+s by using 118 | # the +:call+ as +func+ parameter. 119 | # 120 | # The following example illustrates the use of a lambda: 121 | # 122 | # require 'observer' 123 | # 124 | # class Ticker 125 | # include Observable 126 | # 127 | # def run 128 | # # logic to retrieve the price (here 77.0) 129 | # changed 130 | # notify_observers(77.0) 131 | # end 132 | # end 133 | # 134 | # ticker = Ticker.new 135 | # warner = ->(price) { puts "New price received: #{price}" } 136 | # ticker.add_observer(warner, :call) 137 | # ticker.run 138 | module Observable 139 | VERSION = "0.1.2" 140 | 141 | # 142 | # Add +observer+ as an observer on this object. So that it will receive 143 | # notifications. 144 | # 145 | # +observer+:: the object that will be notified of changes. 146 | # +func+:: Symbol naming the method that will be called when this Observable 147 | # has changes. 148 | # 149 | # This method must return true for +observer.respond_to?+ and will 150 | # receive *arg when #notify_observers is called, where 151 | # *arg is the value passed to #notify_observers by this 152 | # Observable 153 | def add_observer(observer, func=:update) 154 | @observer_peers = {} unless defined? @observer_peers 155 | unless observer.respond_to? func 156 | raise NoMethodError, "observer does not respond to `#{func}'" 157 | end 158 | @observer_peers[observer] = func 159 | end 160 | 161 | # 162 | # Remove +observer+ as an observer on this object so that it will no longer 163 | # receive notifications. 164 | # 165 | # +observer+:: An observer of this Observable 166 | def delete_observer(observer) 167 | @observer_peers.delete observer if defined? @observer_peers 168 | end 169 | 170 | # 171 | # Remove all observers associated with this object. 172 | # 173 | def delete_observers 174 | @observer_peers.clear if defined? @observer_peers 175 | end 176 | 177 | # 178 | # Return the number of observers associated with this object. 179 | # 180 | def count_observers 181 | if defined? @observer_peers 182 | @observer_peers.size 183 | else 184 | 0 185 | end 186 | end 187 | 188 | # 189 | # Set the changed state of this object. Notifications will be sent only if 190 | # the changed +state+ is +true+. 191 | # 192 | # +state+:: Boolean indicating the changed state of this Observable. 193 | # 194 | def changed(state=true) 195 | @observer_state = state 196 | end 197 | 198 | # 199 | # Returns true if this object's state has been changed since the last 200 | # #notify_observers call. 201 | # 202 | def changed? 203 | if defined? @observer_state and @observer_state 204 | true 205 | else 206 | false 207 | end 208 | end 209 | 210 | # 211 | # Notify observers of a change in state *if* this object's changed state is 212 | # +true+. 213 | # 214 | # This will invoke the method named in #add_observer, passing *arg. 215 | # The changed state is then set to +false+. 216 | # 217 | # *arg:: Any arguments to pass to the observers. 218 | def notify_observers(*arg) 219 | if defined? @observer_state and @observer_state 220 | if defined? @observer_peers 221 | @observer_peers.each do |k, v| 222 | k.__send__(v, *arg) 223 | end 224 | end 225 | @observer_state = false 226 | end 227 | end 228 | 229 | end 230 | --------------------------------------------------------------------------------