In Progress
Unit 1, Lesson 1
In Progress

Video transcript & code

In episode #279, we rewrote a complex predicate method so that it could take an optional "auditor" argument. The auditor is able to surface details about the decisions that went into the predicate's conclusion, in a user-visible and understandable way.

``````require "active_support"
require "active_support/core_ext"

class Reminder

def initialize(completed: false, reminder_at:, last_reminded_at: nil)
@completed = completed
@reminder_at = reminder_at
@last_reminded_at = last_reminded_at
end

alias completed? completed

return false if     audit.("task completed") { completed? }
return false unless audit.("past due") { current_time_is_past_due_time? }
return false if     audit.("sent yesterday") { sent_yesterday? }
return false unless audit.("hour late enough") { hour_is_past? }
return false unless audit.("minute late enough") { minute_is_past? }
true
end

def current_time_is_past_due_time?
reminder_at <= Time.current
end

def sent_yesterday?
last_reminded_at != nil && last_reminded_at > 23.hours.ago
end

def hour_is_past?
reminder_at.hour <= Time.current.hour
end

def minute_is_past?
reminder_at.min <= Time.current.min
end
end
``````

As a a demonstration, here's the simplest example we used in that episode. We have a little lambda that simply takes the description and the result of each criterion in the predicate, and prints them to standard out.

``````require "./reminder"

r = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 22, 0))

auditor = ->(description, &test) {
result = test.call
puts "#{description}: #{result}"
result
}

# >> past due: true
# >> sent yesterday: false
# >> hour late enough: false
``````

This is just one example. Since auditors can be any callable object, the sky is the limit when it comes to what we do with the information they collect. It would be easy, for instance, to write an auditor that collected information to display as a validation error on a form.

Today I want to discuss some advanced considerations for this "audited predicate" pattern.

First off, what happens when we want to audit more than one method deep?

Here's an example. Let's say we decide to extract the last two tests in our predicate to a method called `late_enough_to_alert?`.

``````class Reminder
# ...
return false unless audit.("hour late enough") { hour_is_past? }
return false unless audit.("minute late enough") { minute_is_past? }
true
end
# ...
end
``````

How should we alter the main composite predicate method? The most straightforward answer is that we add a call to the `late_enough_to_alert?` predicate, and pass in our auditor.

``````require "./reminder"

class Reminder
# ...
return false if     audit.("task completed") { completed? }
return false unless audit.("past due") { current_time_is_past_due_time? }
return false if     audit.("sent yesterday") { sent_yesterday? }
true
end
# ...
end
``````

When we run our example code again, the output is identical to what we saw before.

``````require "./reminder2"

r = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 22, 0))

auditor = ->(description, &test) {
result = test.call
puts "#{description}: #{result}"
result
}

# >> past due: true
# >> sent yesterday: false
# >> hour late enough: false
``````

Before we move forward, let's eliminate a little bit of duplication. Both of these methods use the same inline lambda for their default auditor. Repeating this definition over and over could get annoying fast. Let's introduce an `Auditing` module, and give it a singleton method which will return this default auditor.

``````module Auditing
def self.default_auditor
->(*, &block){block.call}
end
end
``````

Then we replace both instances of the inline lambda definition with calls to this new global method.

``````require "./reminder2"
require "./auditing"

class Reminder
# ...
return false unless audit.("hour late enough") { hour_is_past? }
return false unless audit.("minute late enough") { minute_is_past? }
true
end

return false if     audit.("task completed") { completed? }
return false unless audit.("past due") { current_time_is_past_due_time? }
return false if     audit.("sent yesterday") { sent_yesterday? }
true
end
# ...
end
``````

This takes care of our little duplication problem.

Now let's introduce a new complication. In another part of the program, we have a helper method that takes a list of reminders and checks to see if any of them are due for alerts. This method was written before we added auditing to reminders, and it doesn't know anything about it.

``````def any_due?(reminders)
end
``````

Here's a quick demo of how it works. We instantiate two different reminders, and then pass them in an array to the helper method. It returns true if any of the reminders is due for an alert.

``````require "./reminder3"
require "./any_due"

r1 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 22, 0))

r2 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 1, 0))

any_due?([r1, r2])              # => true
``````

If we want to audit the decisions made within this method, we have a problem. It doesn't know anything about auditors, and can't pass them through.

Now, obviously one solution would be to modify the `any_due?` method. But in the real world that might not be feasible. This example only has as single, simple method in between us and direct interaction with `Reminder` objects. But in a realistic scenario, there might be a half a dozen or more levels of call between here and the methods we want to audit. And some of those levels might be in third-party gem code that we can't change.

In effect, we need to find a way to effectively "tunnel" information down deeper into the call stack, without modifying the intervening methods. This might ring a bell: we talked about a technique for doing exactly that in episode #161.

Let's reconsider our whole approach to dealing with multiple levels of method that need to be audited. First, we're going to modify the `alert_due?` method. No longer will we pass the auditor down into the next layer of predicates. Instead, the call the the method we extracted will look exactly like the calls to all the other lower-level predicates.

``````require "./reminder3"

class Reminder
# ...
return false if     audit.("task completed") { completed? }
return false unless audit.("past due") { current_time_is_past_due_time? }
return false if     audit.("sent yesterday") { sent_yesterday? }
true
end
# ...
end
``````

Now we turn our attention to the `Auditing` module. We start by adding a new method called `thread_local_state`. This method stores a hash in a thread-local variable, or returns a pre-existing hash if it has already been initialized.

Next we update the definition of `.default_auditor`. We make it return either the value of a `:current_auditor` key inside the thread local storage, or, if none exists, the default do-nothing lambda we used before.

Now we define another new method. This one is called `with_auditor`. It takes an auditor object as an argument. Inside, it first saves off any existing current auditor, if any. Then it updates the current auditor using the passed-in object. Then it yields to caller code. After caller code is done, it cleans up by resetting the current auditor to the old value, which might have been `nil`.

``````module Auditing
def self.default_auditor
end

def self.with_auditor(auditor)
yield
ensure
end

end
end
``````

Let's see how we can put this to use. We build an auditor. Then we construct a `with_auditor` block around our use of the `any_due?` predicate. When we execute the code, we can now see our auditing output for each reminder that was processed. We've successfully passed an auditor down the call chain without having to update any intervening code.

``````require "./reminder4"
require "./any_due"
require "./auditing2.rb"

r1 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 22, 0))

r2 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 1, 0))

auditor = ->(description, &test) {
result = test.call
puts "#{description}: #{result}"
result
}

Auditing.with_auditor(auditor) do
any_due?([r1, r2])              # => true
end

# >> past due: true
# >> sent yesterday: false
# >> hour late enough: false
# >> late enough to alert: false
# >> past due: true
# >> sent yesterday: false
# >> hour late enough: true
# >> minute late enough: true
# >> late enough to alert: true
``````

Let's look at one more variation on this theme before we stop.

This time, we're going to add a new singleton-level reader and writer for a thread-local value called `level`. We also add a reader for the current auditor.

Then we add an instance method to the `Auditing` module. We call it, simply, `#audit`. We have it take a description and a block.

Inside, it first increments the current level. Then it delegates auditing of the passed block to the current auditor. Afterwards, it decrements the current level.

``````module Auditing
def self.default_auditor
end

def self.with_auditor(auditor)
yield
ensure
end

def self.level
end

def self.level=(new_level)
end

def self.current_auditor
end

end

def audit(description, &block)
Auditing.level += 1
Auditing.current_auditor.call(description, &block)
ensure
Auditing.level -= 1
end
end
``````

Back in our reminder class, we change things up. For the first time, we include the `Auditing` module into the class. Then for each method that uses auditing, we remove the arguments and change the audited lines from using object call notation to use ordinary method calls to the `#audit` method we defined moments ago.

``````require "./reminder4"
require "./auditing3"

class Reminder
include Auditing

# ...

return false unless audit("hour late enough") { hour_is_past? }
return false unless audit("minute late enough") { minute_is_past? }
true
end

return false if     audit("task completed") { completed? }
return false unless audit("past due") { current_time_is_past_due_time? }
return false if     audit("sent yesterday") { sent_yesterday? }
return false unless audit("late enough") { late_enough_to_alert? }
true
end
# ...
end
``````

Now we update our standard output auditor a little bit. We add a prefix of star characters before each output line, with the number of stars based on the current auditing level.

When we run this code, we can see not only the decisions that went into it, but we can see which ones were at the top level, and which ones were subsidiary parts of higher-level predicates.

``````require "./reminder5"
require "./any_due"

r1 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 22, 0))

r2 = Reminder.new(completed: false,
reminder_at: Time.new(2014, 1, 15, 1, 0))

auditor = ->(description, &test) {
result = test.call
stars = "*" * Auditing.level
puts "#{stars} #{description}: #{result}"
result
}

Auditing.with_auditor(auditor) do
any_due?([r1, r2])              # => true
end

# >> * task completed: false
# >> * past due: true
# >> * sent yesterday: false
# >> ** hour late enough: false
# >> * late enough: false
# >> * task completed: false
# >> * past due: true
# >> * sent yesterday: false
# >> ** hour late enough: true
# >> ** minute late enough: true
# >> * late enough: true
``````

There are other variations on this theme we could explore, but I think that's enough for today. Happy hacking!