“Bundler: The best way to manage a Ruby application's dependencies”
We are going to use a bundle gem
command which creates the skeleton for our rubygem
Rubygem: is a standard format for Ruby programs and libraries
$ gem install bundler
...
$ bundle gem metaprogramming_example --test
Creating gem 'metaprogramming_example'...
MIT License enabled in config
create metaprogramming_example/Gemfile
create metaprogramming_example/.gitignore
create metaprogramming_example/lib/metaprogramming_example.rb
create metaprogramming_example/lib/metaprogramming_example/version.rb
create metaprogramming_example/metaprogramming_example.gemspec
create metaprogramming_example/Rakefile
create metaprogramming_example/README.md
create metaprogramming_example/bin/console
create metaprogramming_example/bin/setup
create metaprogramming_example/.travis.yml
create metaprogramming_example/.rspec
create metaprogramming_example/spec/spec_helper.rb
create metaprogramming_example/spec/metaprogramming_example_spec.rb
create metaprogramming_example/LICENSE.txt
Initializing git repo in /Users/karim/code/tmp/metaprogramming_example
cd
into the newly created gem directory, and type rake -T
:
You'll find that Bundler created few Rake tasks to make our life a little bit easier.
$ cd metaprogramming_example
$ rake -T
rake build # Build metaprogramming_example-0.1.0.gem into the pkg directory
rake clean # Remove any temporary products
rake clobber # Remove any generated files
rake install # Build and install metaprogramming_example-0.1.0.gem into system gems
rake install:local # Build and install metaprogramming_example-0.1.0.gem into system gems without network access
rake release[remote] # Create tag v0.1.0 and build and push metaprogramming_example-0.1.0.gem to Rubygems
rake spec # Run RSpec code examples
Is it ...
“Metaprogramming is the writing of computer programs with the ability to treat programs as their data. It means that a program could be designed to read, generate, analyse or transform other programs, and even modify itself while running.”
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = data_source.get_mouse_info(id)
price = data_source.get_mouse_price(id)
"Mouse: #{info} ($#{price})"
end
def cpu
info = data_source.get_cpu_info(id)
price = data_source.get_cpu_price(id)
"CPU: #{info} ($#{price})"
end
...
end
Is this code fully tested?
obj.method
=> something
What if we want to do something like this?
array = [:one, :two, :three]
array.each do |item|
obj.item # This won't work
end
We'll have to use Dynamic Dispatch technique (send
)
array.each do |item|
obj.send item # this will work
end
* send
can accept: strings and symbols as a method name
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
...
def component(name)
info = data_source.send("get_#{name}_info", id)
price = data_source.send("get_#{name}_price", id)
"#{name.capitalize}: #{info} ($#{price})"
end
end
Object#send
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def memory
component :memory
end
def motherboard
component :motherboard
end
def storage
component :storage
end
So, in the case we need to define lots of similar methods, Dynamic Method is the way to go.
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) do
info = data_source.send("get_#{name}_info", id)
price = data_source.send("get_#{name}_price", id)
"#{name.capitalize}: #{info} ($#{price})"
end
end
define_component :mouse
...
end
Module#define_method
define_component
method lots of times
define_component :mouse
define_component :cpu
define_component :keyboard
define_component :memory
define_component :motherboard
define_component :storage
What about doing this?
[:mouse, :cpu, :keyboard, :memory, :motherboard, :storage].each do |component|
define_component component
end
A bit better, still this won't scale nicely, right?
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
end
def self.define_component(name)
define_method(name) do
info = data_source.send("get_#{name}_info", id)
price = data_source.send("get_#{name}_price", id)
"#{name.capitalize}: #{info} ($#{price})"
end
end
end
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = data_source.get_mouse_info(id)
price = data_source.get_mouse_price(id)
"Mouse: #{info} ($#{price})"
end
def cpu
info = data_source.get_cpu_info(id)
price = data_source.get_cpu_price(id)
"CPU: #{info} ($#{price})"
end
...
end
method_missing
and we get NoMethodError
> class Useless; end
> x = Useless.new.whatever
NoMethodError: undefined method `whatever' for #Useless:0x007f934301f580
What if we can override method_missing
> class Useless
> def method_missing(method)
> puts "Oh! No! #{method} is not defined"
> end
> end
Now...
> x = Useless.new.whatever
> Oh! No! whatever is not defined
So, we should be able to use this technique to define our methods on run-time.
class Computer
attr_reader :id, :data_source
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
super unless respond_to? name
info = data_source.send("get_#{name}_info", id)
price = data_source.send("get_#{name}_price", id)
"#{name.capitalize}: #{info} ($#{price})"
end
def respond_to_missing?(method, include_private = false)
data_source.respond_to?("get_#{method}_info") || super
end
end
BasicObject#method_missing, Object#respond_to?, Object#respond_to_missing?
You can follow a simple rule of thumb when in doubt:
use Dynamic Methods if you can and Ghost Methods if you have to.
def method_missing(name)
super unless respond_to? name
info = data_source.send("get_#{name}_info", id)
price = data_source.send("get_#{name}_price", id)
"#{name.capitalize}: #{info} ($#{price})"
end
def respond_to_missing?(method, include_private = false)
data_source.respond_to?("get_#{method}_info") || super
end
> irb
> class String
> def upcase?
> self == self.upcase
> end
> end
> 'karim'.upcase?
=> false
> 'KARIM'.upcase?
=> true