Dynamic dispatch is how conditional logic travels backwards in time
What is dynamic dispatch?
Dynamic dispatch is when your language decides at run time which actual method to call. For example:
class Foo
def say
puts "foo"
end
end
class Bar < Foo
def say
puts "bar"
end
end
def make_noise(o)
o.say
end
make_noise(Foo.new) # => "foo"
make_noise(Bar.new) # => "bar"
On encoutering o.say
, Ruby decides at runtime, based on o’s type,
whether to call Foo#say
or Bar#say
. That’s all dynamic dispatch
is. I remember it being a big deal to C programmers when they first
saw C++, but it’s pretty routine now.
Logging without time travel
Let’s have an object that needs to do some logging, or not.
class Foo
def initialize(verbose)
@verbose = verbose
end
def bar
print "bar starting..." if @verbose
# ...
puts " done" if @verbose
end
end
This is no good because we keep repeating if @verbose
. We have
duplicated how to decide whether or not to log.
This class also has too many concerns. It must do whatever it is that a Foo does, and it must be concerened with how and whether log. We can fix the duplication, and give this class less responsibility.
Logging gets a class of its own (but still no time travel)
Let’s move how and whether to log into its own class:
class Log
def initialize(verbose)
@verbose = verbose
end
def puts(s)
puts s if @verbose
end
def print(s)
print s if @verbose
end
end
Foo now takes a log:
class Foo
def initialize(log)
@log = log
end
def bar
@log.print "bar starting..."
# ...
@log.puts " done"
end
end
And in use:
def use_foo(verbose)
log = Log.new(verbose)
Foo.new(log).bar
end
use_foo(true) # => "bar starting... done"
use_foo(false) # =>
This is much better. How and whether to log is now in its own class.
But look at these two lines:
puts s if @verbose
...
print s if @verbose
A condition that is repeated may be a sign of code that can benefit from polymorphism. Let’s see how.
Applying the Null Object pattern
Let’s apply the null object pattern to the logger and see what happens. We’ll remove all of the conditional code from Log:
class Log
def puts(s)
puts s
end
def print(s)
print s
end
end
and introduce a NullLog which has the same signature, but does nothing:
class NullLog
def puts(s)
end
def print(s)
end
end
Then, in use:
def use_foo(verbose)
log = (verbose ? Log : NullLog).new
Foo.new(log).bar
end
use_foo(true) # => "bar starting... done"
use_foo(false) # =>
The Log class has retained the knowledge of how to log, but it no longer is responsible for knowing whether to log. We’ve given that responsibility to the class that is creating the log instance.
There’s the time travel
Through the use of polymorphism and dynamic dispatch, we have “time-traveled” the decision of whether to log from when the logging is done to earlier in the program’s execution, when the log object was made.
A Factory
There’s another pattern I would usually apply here. The creation of the Log or NullLog can be moved into a factory method:
class Log
def self.make(verbose)
(verbose ? self : NullLog).new
end
...
end
and in use:
def use_foo(verbose)
log = Log.make(verbose)
Foo.new(log).bar
end
use_foo(true) # => "bar starting... done"
use_foo(false) # =>