Message Passing
Introduction
Section titled “Introduction”In Object Oriented Design, objects receive messages and reply to them. In Ruby, sending a message is calling a method and result of that method is the reply.
In Ruby message passing is dynamic. When a message arrives rather than knowing exactly how to reply to it Ruby uses a predefined set of rules to find a method that can reply to it. We can use these rules to interrupt and reply to the message, send it to another object or modify it among other actions.
Each time an object receives a message Ruby checks:
- If this object has a singleton class and it can reply to this message.
- Looks up this object’s class then class’ ancestors chain.
- One by one checks if a method is available on this ancestor and moves up the chain.
Message Passing Through Inheritance Chain
Section titled “Message Passing Through Inheritance Chain”class Example def example_method :example end
def subexample_method :example end
def not_missed_method :example end
def method_missing name return :example if name == :missing_example_method return :example if name == :missing_subexample_method return :subexample if name == :not_missed_method super endend
class SubExample < Example def subexample_method :subexample end
def method_missing name return :subexample if name == :missing_subexample_method return :subexample if name == :not_missed_method super endend
s = Subexample.newTo find a suitable method for SubExample#subexample_method Ruby first looks at ancestors chain of SubExample
SubExample.ancestors # => [SubExample, Example, Object, Kernel, BasicObject]It starts from SubExample. If we send subexample_method message Ruby chooses the one available one SubExample and ignores Example#subexample_method.
s.subexample_method # => :subexampleAfter SubExample it checks Example. If we send example_method Ruby checks if SubExample can reply to it or not and since it can’t Ruby goes up the chain and looks into Example.
s.example_method # => :exampleAfter Ruby checks all defined methods then it runs method_missing to see if it can reply or not. If we send missing_subexample_method Ruby won’t be able to find a defined method on SubExample so it moves up to Example. It can’t find a defined method on Example or any other class higher in chain either. Ruby starts over and runs method_missing. method_missing of SubExample can reply to missing_subexample_method.
s.missing_subexample_method # => :subexampleHowever if a method is defined Ruby uses defined version even if it is higher in the chain. For example if we send not_missed_method even though method_missing of SubExample can reply to it Ruby walks up on SubExample because it doesn’t have a defined method with that name and looks into Example which has one.
s.not_missed_method # => :exampleMessage Passing Through Module Composition
Section titled “Message Passing Through Module Composition”Ruby moves up on ancestors chain of an object. This chain can contain both modules and classes. Same rules about moving up the chain apply to modules as well.
class Exampleend
module Prepended def initialize *args return super :default if args.empty? super endend
module FirstIncluded def foo :first endend
module SecondIncluded def foo :second endend
class SubExample < Example prepend Prepended include FirstIncluded include SecondIncluded
def initialize data = :subexample puts data endend
SubExample.ancestors # => [Prepended, SubExample, SecondIncluded, FirstIncluded, Example, Object, Kernel, BasicObject]
s = SubExample.new # => :defaults.foo # => :secondInterrupting Messages
Section titled “Interrupting Messages”There are two ways to interrupt messages.
- Use
method_missingto interrupt any non defined message. - Define a method in middle of a chain to intercept the message
After interrupting messages, it is possible to:
- Reply to them.
- Send them somewhere else.
- Modify the message or its result.
Interrupting via method_missing and replying to message:
class Example def foo @foo end
def method_missing name, data return super unless name.to_s =~ /=$/ name = name.to_s.sub(/=$/, "") instance_variable_set "@#{name}", data endend
e = Example.new
e.foo = :fooe.foo # => :fooIntercepting message and modifying it:
class Example def initialize title, body endend
class SubExample < ExampleendNow let’s imagine our data is “title:body” and we have to split them before calling Example. We can define initialize on SubExample.
class SubExample < Example def initialize raw_data processed_data = raw_data.split ":"
super processed_data[0], processed_data[1] endendIntercepting message and sending it to another object:
class ObscureLogicProcessor def process data :ok endend
class NormalLogicProcessor def process data :not_ok endend
class WrapperProcessor < NormalLogicProcessor def process data return ObscureLogicProcessor.new.process data if data.obscure?
super endend