Observer Pattern

lua-users home
wiki

This is an implementation of the Observer Design Pattern [1], making use of closures.

-- Observer design pattern.
-- Produces a few functions that have a closure that implements
-- subject-observer notification.

-- Example usage:
-- subject.reg, subject.dereg, subject.notify = observer.create()
-- subject:reg( "signal", observer, method )
-- subject:reg( "signal", nil, function )
-- subject:notify( "signal" )
-- subject:dereg( "signal", observer, method )
-- subject:dereg( "signal", nil, function )

local table = require("table");
local base = require("base");
local setmetatable = base.setmetatable
local ipairs = base.ipairs

module("observer")

function create()
  -- Containter holds the filter, and handlers and observers.
  local container = {}

  -- Provision for garbage collection, weak metatable and sentinel.
  local weak = { __mode = "kv" }

  -- Register
  -- Creates an observation between the Subject data and the
  -- Observer data, using filter Signal and handler Method.
  -- Usage:
  --   s:register( "update", o, o.m )
  local register = function( subject, signal, observer, method )
    t = container[signal] or {}
    local o = observer or weak
    local k = { method, o }
    setmetatable( k, weak )
    table.insert( t, k )
    container[signal] = t
  end

  -- Deregister
  -- Removes any observations in the Signal filter, either matching
  -- the Observer and Method, or the whole filter if both are nil.
  -- Usage:
  --   s:deregister( "update" )
  --   s:deregister( "update", o )
  --   s:deregister( "update", o, o.m )
  local deregister = function( subject, signal, observer, method )
    t = container[signal]
    if not t then return end
    if not method and not observer then
      container[signal] = nil
      return
    end
    local i, v
    i = #t
    while i > 0 do
      v = t[i] or {}
      if  ( not method   or v[1] == method )
      and ( not observer or v[2] == observer ) then
        table.remove( t, i )
      end
      i = i - 1
    end
  end

  -- Notify
  -- Uses the Signal Filter to notify all observations via their
  -- registered handlers.
  -- Usage:
  --   s:notify( "update" )
  local notify = function( subject, signal, ... )
    t = container[signal]
    if not t then return end
    for i, v in ipairs( t ) do
      if v[2] == weak then
        v[1](subject, ...)
      elseif v[2] then
        v[1](v[2], subject, ...)
      end
      -- garbage collected observers (nil) are skipped.
    end
  end

  return register, deregister, notify
end

-- Signal
-- Convienience function that gives a shorthand to sending a
-- notification.
-- Usage:
--   s.update = signal( s, s.notify, "update" )
--   s.update()
function signal( subject, notify, name )
  return function( ... )
    return notify( subject, name, ... )
  end
end

Here's an example usage. Note that the observer.signal function helps create a syntax that somewhat imitates the signals and slots model of the C++ framework Qt.

require("observer")

-- Observers.
function a( subject )
  print( "a", type(subject) )
end

function b( data, subject, extra )
  print( "b", data, type(subject), extra )
end

o = {}
function o:m( subject, extra )
  print( "o:m", type(self), type(subject), extra )
end

-- First Observation
reg, dereg, notify = observer.create()

reg( nil, "signal", nil, a )
reg( nil, "signal", "lol", b )
print( "First notification" )
notify( nil, "signal", "zomg" )
dereg( nil, "signal", nil, a )
print( "Second notification" )
notify( nil, "signal" )

-- Second Observation

s = {}
s.reg, s.dereg, s.notify = observer.create()
s.signal = observer.signal( s, s.notify, "signal" )

s:reg( "signal", nil, a )
s:reg( "signal", o, o.m )
print( "Third notification" )
s.signal("rofl")
s:dereg( "signal" )

s:reg( "signal", o, o.m )
print( "Fourth notification" )
o = nil
collectgarbage()
s:notify( "signal" )

comments appreciated.

--WilliamBubel


RecentChanges · preferences
edit · history
Last edited September 16, 2008 2:07 am GMT (diff)