Observable 모듈

Observable 모듈

Observer 패턴(발행/구독이라고도 불러요)은 한 객체가 자기 상태가 바뀌었을 때 관심 있는 여러 제3자 객체에게 그 사실을 알려주는 간단한 메커니즘이에요.

출처: Ruby 3.3 API

본문

동작 방식 (Mechanism)

알림을 보내는 클래스가 Observable 모듈을 믹스인하면, 그 클래스는 연관된 observer 객체들을 관리하는 메서드들을 갖게 돼요.

observable 객체는 반드시:

  • 상태가 바뀌었음을 알리기 위해 #changed를 호출해야 하고
  • #notify_observers를 호출해야 해요.

observer는 Observable#add_observer로 업데이트를 구독하는데, 이때 notify_observers를 통해 호출될 메서드도 함께 지정해요. notify_observers의 기본 메서드는 update예요.

예시 (Example)

다음 예시가 이 패턴을 잘 보여줘요. Ticker는 실행되면 @symbol의 주식 Price를 계속 받아요. Warner는 가격의 일반적인 observer이고, WarnLowWarnHigh 두 워너를 보여 줘요. 각각 가격이 정해둔 한계보다 낮거나 높으면 경고를 출력해요.

update 콜백 덕분에 워너들은 명시적으로 호출되지 않아도 스스로 동작해요. 시스템은 Ticker와 여러 observer로 구성되어 있고, 최상위 코드가 개입하지 않아도 observer들이 자기 역할을 해내요.

참고로 발행자와 구독자(observable과 observer) 사이의 계약(contract)은 선언되거나 강제되지 않아요. Ticker는 시간과 가격을 발행하고 워너들이 그걸 받아요. 하지만 계약이 올바른지 직접 보장하지 않으면, 그 누구도 당신에게 경고해 주지 않아요.

require "observer"

class Ticker          ### Periodically fetch a stock price.
  include Observable

  def initialize(symbol)
    @symbol = symbol
  end

  def run
    last_price = nil
    loop do
      price = Price.fetch(@symbol)
      print "Current price: #{price}\n"
      if price != last_price
        changed                 # notify observers
        last_price = price
        notify_observers(Time.now, price)
      end
      sleep 1
    end
  end
end

class Price           ### A mock class to fetch a stock price (60 - 140).
  def self.fetch(symbol)
    60 + rand(80)
  end
end

class Warner          ### An abstract observer of Ticker objects.
  def initialize(ticker, limit)
    @limit = limit
    ticker.add_observer(self)
  end
end

class WarnLow < Warner
  def update(time, price)       # callback for observer
    if price < @limit
      print "--- #{time.to_s}: Price below #@limit: #{price}\n"
    end
  end
end

class WarnHigh < Warner
  def update(time, price)       # callback for observer
    if price > @limit
      print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
    end
  end
end

ticker = Ticker.new("MSFT")
WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
ticker.run

이런 출력을 만들어 내요.

Current price: 83
Current price: 75
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79

proc과 함께 쓰기 (Usage with procs)

#notify_observers 메서드는 func 파라미터로 :call을 사용하면 proc과도 함께 쓸 수 있어요.

다음 예시는 lambda 사용을 보여줘요.

require 'observer'

class Ticker
  include Observable

  def run
    # logic to retrieve the price (here 77.0)
    changed
    notify_observers(77.0)
  end
end

ticker = Ticker.new
warner = ->(price) { puts "New price received: #{price}" }
ticker.add_observer(warner, :call)
ticker.run

상수 (Constants)

  • VERSION

Public Instance Methods

add_observer(observer, func=:update)

observer를 이 객체의 observer로 추가해요. 그래야 notification을 받을 수 있어요.

observer: 변경을 통보받을 객체예요. func: 이 Observable에 변경이 있을 때 호출될 메서드를 가리키는 Symbol이에요. 이 메서드는 observer.respond_to?에 대해 true를 반환해야 하고, notify_observers가 호출되면 *arg를 받아요. 여기서 *arg는 이 Observablenotify_observers에 넘겨준 값이에요.

changed(state=true)

이 객체의 changed 상태를 설정해요. notification은 changed statetrue일 때만 보내져요.

state: 이 Observable의 changed 상태를 나타내는 boolean이에요.

changed?()

마지막 notify_observers 호출 이후로 이 객체의 상태가 변경됐으면 true를 반환해요.

count_observers()

이 객체와 연관된 observer의 수를 반환해요.

delete_observer(observer)

observer를 이 객체의 observer에서 제거해서 더 이상 notification을 받지 않게 해요.

observer: 이 Observable의 observer예요.

delete_observers()

이 객체와 연관된 모든 observer를 제거해요.

notify_observers(*arg)

이 객체의 changed 상태가 true이면 상태 변경을 observer에게 알려요.

add_observer에 지정된 메서드를 *arg를 넘겨 호출해요. 그 후 changed 상태는 false로 설정돼요.

*arg: observer에게 전달할 인자들이에요.