Ruby modules, the template pattern, and dynamic closure application
This post demonstrates how to apply a set of closures to various components (i.e. Ruby on Rails controllers) in a very flexible, indirect, and dynamic fashion. The final result involves using modules to implement a template pattern and invoking closures bound to new contexts.
Abusing Kernel#caller (remembering Java Reflection)
I recently had an issue where I needed to know the caller of a method. The goal was to apply one or more closures cached in a singleton configuration object to a controller or other component. I was originally on the right track, but I got sidetracked by a google search. I was reminded of Kernel#caller. The mess that ensued looked like this:
1 require 'active_support/inflector'
2
3 module Arbitrary2
4 class MyController
5
6 def some_action
7 NeedsToKnowCaller.apply_config
8 end
9
10 end
11 end
12
13 module NeedsToKnowCaller
14
15 SIG = /controllers\/([^\.]*).*`([^']*)'/
16
17 class << self
18
19 # overriding Kernel#caller only for sake of repeatability on blog
20 # during normal use this override would not exist
21 def caller
22 ["/arbitrary/project/app/controllers/arbitrary2/my_controller.rb:20:in `some_action'"]
23 end
24
25 # extract signature for use elsewhere...
26 # dont want to explicitly declare caller as a param but could...
27 def apply_config
28 md = caller[0].match(SIG)
29 sig = "#{md[1].classify}.#{md[2]}"
30 # could do something with eval here... timeout...
31 puts sig
32 end
33
34 end
35 end
36
37 Arbitrary2::MyController.new.some_action
38 # printed value is "Arbitrary2::MyController.some_action"
What happens here is that some_action is called in an instance of MyController. This calls apply_config in the NeedsToKnowCaller module which is able to look up the class that called it. This implementation is left incomplete. It is not at all a dead end, but it is an expensive and unnecessary bad road to continue down.
Ruby Modules and the Template Design Pattern
As soon as I was finished tracking down the caller I remembered what I was really trying to do in the first place. I did not just need the name of the caller. I needed the context of the caller as well so that I could invoke a closure in the context of the caller. The solution above is terrible. I blame my history with Java reflection for this brief lapse of judgement. For anyone looking up Kernel#caller or a regex to be used with it, please stop right here. Ruby has better options in the form of modules.
Modules can be used for much more than just bags of functions or namespaces. The template pattern typically looks a little different than this, but that is essentially what I ended up reinventing. Instead of using inheritance, I ended up using mixins in a slightly different way than I usually do.
1 module NeedsToKnowCaller
2
3 class << self
4
5 # actual impl much more complex...
6 def config(key)
7 # hard coded for clarity
8 -> { root_path }
9 end
10
11 def apply_config_to _caller, key
12 _caller.instance_exec &config(key)
13 end
14
15 end
16
17 module ControllerMethods
18
19 # dont want to explicitly declare caller as a param but could...
20 def apply_config(key)
21 NeedsToKnowCaller.apply_config_to self, key
22 end
23
24 end
25
26 end
27
28 module Arbitrary2
29 class MyController
30 include NeedsToKnowCaller::ControllerMethods
31
32 def some_action
33 apply_config :varies
34 end
35
36 def root_path
37 "would normally come from rails routing"
38 end
39 end
40 end
41
42 Arbitrary2::MyController.new.some_action
43 # => "would normally come from rails routing"
In this example, any controller that includes NeedsToKnowCaller::ControllerMethods will be able to call NeedsToKnowCaller.apply_config_to(self) with just apply_config. Apply_config_to will then call config which returns a lambda which is invoked in the context of the instance that was passed into apply_config_to. So an instance of MyController calls apply_config which indirectly calls root_path. The config method in my actual implementation is much more involved.
This example might suffer from oversimplification. In the context of a large project, small details like cutting a derivable parameter out of a method can be really helpful. In a small project this wouldn't make much sense.
What I do find interesting is how well Ruby facilitates some older design patterns. I learned the template pattern with Java. In my experience it always looks the same in Java. A subclass adds something around a superclass method. Here a collection of semi-related classes call another singleton class that is not inherited at all. The idea is the same. The implementation is totally different. Furthermore, the ease with which closures can be reapplied to other contexts is extremely powerful.