1 obj.bar = ‘2’ obj.bar => 2 class A attr_accessor :foo, :bar end def attr_accessor(*args) args.each do |arg| define_method "#{arg}".to_sym do instance_variable_get("@#{arg}") end define_method "#{arg}=".to_sym do |param| instance_variable_set("@#{arg}", param) end end end
args.each do |arg| define_method "#{arg}".to_sym do instance_variable_get("@#{arg}") end define_method "#{arg}=".to_sym do |param| instance_variable_set("@#{arg}", param) end end end class A def foo @foo end def foo=(str) @foo = str end def bar @bar end def bar=(str) @bar = str end end Readable Concise
Reviewing, Published • Transitions from one state to another r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true Draft Reviewing Published Submit Approve
def reviewing? state == 'reviewing' end def published? state == 'published' end def submit! return unless draft? state = 'reviewing' end def approve! return unless reviewing? state == 'published' end end Why metaprogram? r = Report.new r.state => ‘draft’ r.reviewing? => false r.submit! r.state => ‘reviewing’ r.approve! => ‘published’ r.published? => true What if … Add pre and post-transition hooks Raise errors for incorrect states Metaprogram! Define states and transitions Populate the necessary methods
end class B < A def foo ‘b’ end def bar ‘bar’ end end Class BasicObject Object Superclass A B obj1 Class Class Module Superclass Superclass Superclass Superclass Class obj2 Class Class obj1 = A.new obj2 = B.new obj1.foo => a obj2.foo => b Singleton Class Singleton Class
Good Keep meta-programming code independent! Practise Encapsulation - Try not to break abstraction Don’t meta-program meta-programming code! Meta-programming code should be ‘readable’ More is not always good Maintaining classes with much metaprogramming ain’t easy
['get', 'put', 'post', 'delete'] POSSIBLE_VERBS.each do |m| define_method(m.to_sym) do |path, *args, &b| r[path].public_send(m.to_sym, *args, &b) end end Short != Readable Implicit vs Explicit r = some_object def get(path, *args &b) r[path].get(*args &b) end def put(path, *args &b) r[path].put(*args &b) end def post(path, *args &b) r[path].post(*args &b) end def delete(path, *args &b) r[path].delete(*args &b) end Source: rest-client/rest-client With Meta-programming Without Meta-programming
& Test # attr_accessor(symbol, ...) -> nil # attr_accessor(string, ...) -> nil # # Defines a named attribute for this module, where the name is # <i>symbol.</i><code>id2name</code>, creating an instance variable # (<code>@name</code>) and a corresponding access method to read it. # Also creates a method called <code>name=</code> to set the attribute. # String arguments are converted to symbols. # # module Mod # attr_accessor(:one, :two) # end # Mod.instance_methods.sort #=> [:one, :one=, :two, :two=] def attr_accessor(*several_variants) #This is a stub, used for indexing end Source: Workflow
Workflow workflow do state :draft do event :submit, :transitions_to => :reviewing end state :reviewing do event :publish, :transitions_to => :published end state :published end end test 'question methods for state' do r = Report.first r = assert_state r, 'draft' assert r.draft? assert !r.reviewing? end ... Source: Workflow Write Documentation Commit with confidence Alternate documentation
attr_accessor :states, :events def initialize(&spec) @states, @events = [], {} instance_eval(&spec) end def state(name, &state_spec) @curr_state = name @states << name instance_eval(&state_spec) if state_spec end def event(name, transitions_to: nil) @events[name] = [@curr_state, transitions_to] end end module InstanceMethods def initialize(*args) @workflow_state = self.class.spec&.states[0] super(*args) end def workflow_state @workflow_state end end … module ClassMethods attr_accessor :spec def workflow(&spec) @spec = Spec.new(&spec) populate_methods @spec end def populate_methods(spec) spec&.events&.each do |event, states| module_eval do define_method "#{event}!".to_sym do raise Error if workflow_state.to_sym != states[0].to_sym @workflow_state = states[1].to_sym end end end spec&.states&.each do |curr_state| module_eval do define_method "#{curr_state}?".to_sym do workflow_state.to_sym == curr_state.to_sym end end end end end def self.included(klass) klass.send :include, InstanceMethods klass.extend ClassMethods end end Sample Implementation for Workflow