Not Implemented
Video transcript & code
In the last episode we came up with a LogicalCondition
class. In order to use it, we have to subclass it and provide a condition_holds?
predicate.
require "thread"
module Tapas
class Condition
def initialize(lock)
@lock = lock
@cv = ConditionVariable.new
end
def wait(timeout=nil)
@cv.wait(@lock.mutex, timeout)
end
def signal
@cv.signal
end
end
class Lock
attr_reader :mutex
def initialize
@mutex = Mutex.new
end
def synchronize(&block)
@mutex.synchronize(&block)
end
end
class LogicalCondition
def initialize(client=nil, lock=Lock.new, condition=Condition.new(lock))
@client = client
@lock = lock
@condition = condition
end
def signal
condition.signal
end
def wait(timeout=:never, timeout_policy=->{nil})
deadline = timeout == :never ? :never : Time.now + timeout
@lock.synchronize do
loop do
cv_timeout = timeout == :never ? nil : deadline - Time.now
if !condition_holds? && cv_timeout.to_f >= 0
condition.wait(cv_timeout)
end
if condition_holds?
return yield
elsif deadline == :never || deadline > Time.now
next
else
return timeout_policy.call
end
end
end
end
private
attr_reader :client, :condition
end
end
Here are the two subclasses we derived in that episode. Each overrides condition_holds?
to check for a different state of a Queue
object.
class SpaceAvailableCondition < LogicalCondition
private
def condition_holds?
!client.full?
end
end
class ItemAvailableCondition < LogicalCondition
private
def condition_holds?
!client.empty?
end
end
LogicalCondition
is an abstract class: that is, a class that must be subclassed in order to be used. In some languages, such classes are explicitly denoted by an explicit keyword. In Ruby there is no such annotation. The class is implicitly abstract by dint of the fact that if we use it without subclassing, it won't work.
For instance, if we simply instantiate an instance and try to wait on it, we get a NoMethodError
complaining about the missing predicate.
require "./logical_condition"
cond = Tapas::LogicalCondition.new
cond.wait
# ~> /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:47:in `block (2 levels) in wait': undefined method `condition_holds?' for #<Tapas::LogicalCondition:0x00000001083a58> (NoMethodError)
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:45:in `loop'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:45:in `block in wait'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:27:in `synchronize'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:27:in `synchronize'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition.rb:44:in `wait'
# ~> from -:3:in `<main>'
So how do other programmers know how to use this abstract class? Well, ideally, we'd provide some documentation explaining it. But there's a way we can make their lives a little easier in the code itself.
We can provide a stub implementation of the missing method. Inside this stub, we raise an exception. But not just any exception. We raise NotImplementedError
, which is specifically intended for this scenario. NotImplementedError
inherits from ScriptError
, which in turn inherits directly from Exception
. Since it does not inherit from StandardError
, ordinary rescue clauses will not capture a NotImplementedError
. This is good, because NotImplementedError
indicates a coding defect in the program, and so it shouldn't be handled like a runtime error.
NotImplementedError.ancestors
# => [NotImplementedError,
# ScriptError,
# Exception,
# Object,
# PP::ObjectMixin,
# Kernel,
# BasicObject]
begin
raise NotImplementedError
rescue
puts "Rescued error"
end
# ~> -:2:in `<main>': NotImplementedError (NotImplementedError)
We include an informative error message with the exception.
def condition_holds?
raise NotImplementedError,
"You must implement a #condition_holds? predicate that "
"tests whether the condition is currently true"
end
Now if we try to use a LogicalCondition
without subclassing and overriding #predicate_holds?
, we get a helpful message telling us what we did wrong. But using this technique, users of the class can also read through the class source code and easily see the methods which are required but not provided.
require "./logical_condition2"
cond = Tapas::LogicalCondition.new
cond.wait
# ~> /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:62:in `condition_holds?': You must implement a #condition_holds? predicate that (NotImplementedError)
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:47:in `block (2 levels) in wait'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:45:in `loop'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:45:in `block in wait'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:27:in `synchronize'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:27:in `synchronize'
# ~> from /home/avdi/Dropbox/rubytapas/167-not-implemented/logical_condition2.rb:44:in `wait'
# ~> from -:3:in `<main>'
There are a number of OO patterns where an abstract class or module depends on methods that client code provides. One way to be kind to users of your code is to include "executable documentation" of these abstract methods using stubs that raise NotImplementedError
.
And that's it for today. Happy hacking!
Responses