Method-Hooks provide a way to intercept method calls on objects and can be implemented conveniently using Ruby's meta programming techniques.
Acceptance Test
Starting with an acceptance test helps focusing on the indended usage of the functionality we will implement. Suppose our simple system has the ability to draw content to windows. Windows can be configured via properties in their appearance. Everytime properties are changed the window needs to be redrawn to reflect the changes. In order to ease the handling of windows, we'd like to call redraw automatically when certain properties are changed.
Here's a short implementation of class Window
# Window capable of rendering items
class Window
# Access background color of window.
attr_accessor :background
# Access text messages overlayed to window content
attr_accessor :text
# Simple counter that increments when redraw is invoked
attr_reader :redraw_counter
# Initialize window by drawing content
def initialize
@redraw_counter = 0
end
# Redraw window in case of changes and increment counter
def redraw
#...
@redraw_counter += 1
end
# After setters are invoked update window content
include FollowingHook
following :background=, :text= do |wnd, args|
wnd.redraw
end
end
The most important part about this acceptance test is the syntax we'd like to implement to automatically call redraw when background color or text messages are changed. Here it is again
include FollowingHook
following :background=, :text= do |wnd, args|
wnd.redraw
end
The keyword following is a method name that takes any number of method symbols and a block. Its semantic is to execute the code block immediaetely after the given methods are called. Block arguments are provided: wnd: the window for which one of the provided methods was called, args: given invokation arguments and return value of method.
Advantages are
- Simple generic method hook interface
- Concentrate common code in one place
- Can use provided attribute accessors
Implementation
Here's an implementation of following inside a module FollowingHook
# Contains methods to hook method calls
module FollowingHook
module ClassMethods
private
# Hook the provided instance methods so that the block
# is executed directly after the specified methods have
# been invoked.
#
def following(*syms, &block)
syms.each do |sym| # For each symbol
str_id = "__#{sym}__hooked__"
unless private_instance_methods.include?(str_id)
alias_method str_id, sym # Backup original
# method
private str_id # Make backup private
define_method sym do |*args| # Replace method
ret = __send__ str_id, *args # Invoke backup
block.call(self, # Invoke hook
:method => sym,
:args => args,
:return => ret
)
ret # Forward return value of method
end
end
end
end
end
# On inclusion, we extend the receiver by
# the defined class-methods. This is an ruby
# idiom for defining class methods within a module.
def FollowingHook.included(base)
base.extend(ClassMethods)
end
end
Basically what happens is that for a given method to hook its original content is backup'ed using an unique alias name. The original method content is 'replaced' by an invokation of the original method and an invokation of the given block with current parameters. Additionally we prohibit hooking a method twice and make the original method private to prevent direct execution.
following is used as a class method. To define class methods from within modules we use an inner module that keeps our class methods. When the module is included by the receiver Module#included is invoked with the class as in-parameter. The receiver is then extended by the class methods using Object#extend which causes the receiver class to add following as a class method.
Usage
With our class Window in place we can write a simple unit test to check the behaviour of following
require 'test/unit'
# Tests for methods in PrivateMethodHooks
class TestFollowingHook < Test::Unit::TestCase
# Test following method hook
def test_following_hook
wnd = Window.new
assert_equal(wnd.redraw_counter, 0)
wnd.text = "Show me!"
assert_equal(wnd.redraw_counter, 1)
wnd.background = [1.0, 1.0, 1.0]
assert_equal(wnd.redraw_counter, 2)
end
end
Further Examples
Here's a simple demonstration on how to log all invokations to Kernel#system
class Object
include FollowingHook
following :system do |receiver, args|
p "#{args[:method]} called with arguments #{args[:args].join(",")}"
p "return value was #{args[:return]}"
end
end
system('ruby --version')
# => ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
# => "system called with arguments ruby --version"
# => "return value was true"
# => true
Grab the code!
- following.rb Implementation of FollowingHook module described above
- test_following.rb Unit tests for FollowingHook.
0 Kommentare:
Post a Comment