A little while ago I wrote a blog post titled Convention of Configuration that briefly covered how to take advantage of the constructs provided by Rails. One of the ideas was to take advantage of the environment config options and showed how to add your own config functionality inside the Rails::Initializer block. This has worked very well for us at Primedia, but there is a downside. It has become a little difficult to manage all the different modules contained within this one file. We needed something more manageable.
The first that came to mind was to break out each module into its own file. Sticking with the mycompany.rb convention introduced in the first post, we created a mycompany directory that contained each one of the modules.
Example directory structure:
RAILS_ROOT
|---lib
| |---enhance.rb
| |---mycompany.rb
| | mycompany
| | |----settings.rb
| | |----google.rb
| | |----twitter.rb
| | |----facebook.rb
Our main mycompany.rb could just iterate over the files in that directory and require them, easy peasy. I’ll go into more detail, but here’s the code:
# file: mycompany.rb
$:.unshift(File.dirname(__FILE__))
require "enhance"
module Mycompany
class << self
include Enhance
# Add utility methods here that have high reuse value
# Pass off methods to the Settings enhancement to make the calls
# shorter: Mycompany.settings.adapter vs Mycompany.adapter
def method_missing(method, *args)
settings.send(method, *args)
end
end
end
# Extend functionality of Mycompany module by requiring enhancements
# included in lib/mycompany/*
Dir[File.join( File.dirname(__FILE__), 'mycompany','*.rb')].each do |f|
require f
end
As you can see in the above code, we treat the Settings module a little differently. We would typically store attributes that don’t have or need a namespace to clarify their meaning. What this really means is I don’t want to have to do Mycompany.settings.foo all the time. Using settings doesn’t provide any contextual value and is a waste of time. If you had a Twitter module, then Mycompany.twitter.username does provide contextual value and doesn’t bother me. :)
Now lets look at an example settings file:
# file: mycompany/settings.rb
module Mycompany
module Settings
@@accessors = [ :foo, :bar, :biz, :baz]
class << self
attr_accessor *@@accessors
end
# Initiliaze class instance variables
@foo = []
@biz = {}
end
Mycompany.enhance(Mycompany::Settings)
end
I’m not a fan of class variables, but this looked like the best solution to the problem at hand. Using attr_accessor doesn’t give me an easy way to inspect the module and figure out what attributes have been defined. Using the @@accessors class variable gave me that inspection ability. Nice.
To further clarify, all your modules must use @@accessors to define the accessors.
Now you must be asking, but what is this enhance method doing?
As you probably noticed in mycompany.rb, the enhance module adds class methods to the Mycompany module. Here’s the enhance code:
# file: enhance.rb
module Enhance
def enhance(extension)
accessors = extension.send :class_variable_get, :@@accessors
name = extension.name.split("::").last.downcase
eigenclass = class << self; self; end
eigenclass.class_eval do
# Create a reference to the module
define_method name do # def settings
extension # Mycompany::Settings
end # end
# Enhance the module to include the attributes from the
# extension in the inspect method
define_method "inspect_with_#{name}" do
str = "\"\n\t#{name}:\n#{accessors_inspect_string(name, accessors)}\""
eval(str) << eval("inspect_without_#{name}")
end
end
eigenclass.send :alias_method, "inspect_without_#{name}", :inspect
eigenclass.send :alias_method, :inspect, "inspect_with_#{name}"
end
private
def accessors_inspect_string(name, accessors)
accessors.collect do |a|
"\t\t#{a}: #\{#{name}.#{a}\}\n"
end
end
end
Calling Mycompany.enhance(Mycompany::Settings) will do the following:
- Define a :settings method on Mycompany. e.g. Mycompany.settings
- Enhance the inspect method of Mycompany to include the attributes in the Settings module. example:
settings:
foo: [1,2,3,4]
bar: 'a nice value'
biz: {:a => "one", :b => "two"}
baz: 'ruby rocks'
As you can imagine, adding multiple modules and getting these methods added ‘for free’ and having the Mycompany.inspect show all the attributes for each of the modules is really nice.
Now all you have to do is grab this enhance code, throw it into your project and you can get the same functionality.
If you find a better way to do code what I’ve shown here, please let me know. I would appreciate it