Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Practical Metaprogramming in Application

Practical Metaprogramming in Application

RubyKaigi 2014 talk. Points for *practical* use of metaprogramming in Ruby.

MOROHASHI Kyosuke

September 20, 2014
Tweet

More Decks by MOROHASHI Kyosuke

Other Decks in Programming

Transcript

  1. You will think... Is there more efficient way using repeated

    'title' by manipulating language constructs ?
  2. Just example, real code is written in C. def attr_accessor(name)

    define_method(name) do instance_variable_get("@#{name}") end define_method("#{name}=") do |val| instance_variable_set("@#{name}", val) end end
  3. Manipulate @ivar based on its name define_method(:title) do instance_variable_get("@title") end

    define_method("title=") do |val| instance_variable_set("@title", val) end
  4. “ the ability to examine and modify the structure and

    behavior of the program at runtime Extracted from http://en.wikipedia.org/wiki/Reflection_(computer_programming) Reflection APIs
  5. Sum m ary What is metaprogramming ? Writing code that

    manipulates language constructs So familiar in Ruby Rubyists are using them everyday
  6. DON'T READ: Overuse of metaprogramming Book = Class.new do define_method(:initialize)

    do |attrs| attrs.each do |key, value| if respond_to?("#{key}=") send("#{key}=", value) else instance_variable_set("@#{key}", value) end end end define_method(:price=) do |price_str| instance_variable_set( '@price', price_str.delete(',').to_i ) end end
  7. Standard Ruby code class Book def initialize(attrs) @author = attrs[:author]

    self.price = attrs[:price] end def price=(price_str) @price = Integer(price_str.delete(',')) end end Book.new(author: 'moro', price: '1,980') #=> #<Book:0x007faddb148b48 @author="moro", @price=1980>
  8. Many methods are added class Books < ActiveRecord::Base has_many :reviews

    end Book#reviews #reviews= #review_ids #review_ids= ... Book#reviews.build reviews.create reviews.each {|review| ... } reviews.where(...)
  9. def has_many(name, opts = {}) class_eval <<-RUBY def #{name} klass

    = #{name.to_s.classify} klass.where(#{fk}: id) end def #{name}=(value) values.each do |value| #{name.to_s.classify}.create!(...) end end RUBY en Seems difficult internal (not actual code)
  10. Has owner and target Define join relations on owner.pk =

    target.fk Load target & cache them Manage create/delete and callbacks. Association Object
  11. = Active Record Associations This is the root class of

    all associations ('+ Foo' signifies an included module Foo): Association SingularAssociation HasOneAssociation HasOneThroughAssociation + ThroughAssociation BelongsToAssociation BelongsToPolymorphicAssociation CollectionAssociation HasAndBelongsToManyAssociation HasManyAssociation HasManyThroughAssociation + ThroughAssociation
  12. Define thin wrapper with reflection APIs. def define_readers mixin.class_eval <<-CODE,

    __FILE__, __LINE__ + def #{name}(*args) association(:#{name}).reader(*args) end CODE end
  13. Sum m ary Points for Practical metaprogramming Extract code from

    'meta' aspect & write code simply Define thin wrapper with reflection APIs.
  14. Censors post, comment and photo. Submit each content to external

    censoring service API. Post: title + body Comment: body Photo: public URL
  15. class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service content

    = [title, body].join("\n\n") url = Rails.config.censoring_service_url req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end
  16. class Comment < AR::Base ... def submit_to_censoring_service content = body

    ... end end class Photo < AR::Base ... def submit_to_censoring_service content = "http://img.example.com/#{id}" ... end end
  17. class Post < AR::Base after_save :submit_to_censoring_service private def submit_to_censoring_service content

    = [title, body].join("\n\n") url = Rails.config.censoring_endpoint req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end
  18. class CensorRequest @@url = URI(Rails.config.censoring_endpoint) def initialize(content) @content = content

    end def submit req = Net::HTTP::Post.new(@@url.path) req.set_form_data(content) Net::HTTP.start(@@url.hostname, @@url.port) do http.request(req) end end end
  19. You will think... Extract code from 'meta' aspect There should

    be a class which builds content per each censored class & submits it to the service ?
  20. class ContentCensor def initialize(type, &builder) @type = type @builder =

    builder end def submit(record) CensorRequest.new( id: record.global_id, @type => @builder.call(record) ).submit end end # ---------------- censor = ContentCensor.new(:text) do |post| [post.title, post.body].join("\n\n") end censor.submit(post)
  21. class Post < AR::Base @@censor = ContentCensor.new(:text) do |post| [post.title,

    post.body].join("\n\n") end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end
  22. class Comment < AR::Base @@censor = ContentCensor.new(:text) do |comment| comment.body

    end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end
  23. class Photo < AR::Base @@censor = ContentCensor.new(:image) do |photo| "http://img.example.com/#{photo.id}"

    end after_save :submit_to_censoring_service private def submit_to_censoring_service @@censor.submit(self) end end
  24. You will think... Define thin wrapper with reflection APIs. Does

    extraction of ContentCensor call make the code cleaner ?
  25. You will think... protip: Try to clean up codes like

    Rails framework with ActiveSupport::Concern
  26. module Censorable extend ActiveSupport::Concern module ClassMethods def censor_content(type, &block) censor

    = Censorable::Censor.new(type, block) after_save {|record| censor.submit(record) } end end class Censor ... class Request ... end
  27. Before class Post < ActiveRecord::Base after_save :submit_to_censoring_service private def submit_to_censoring_service

    content = [title, body].join("\n\n") url = Rails.config.censoring_service_url req = Net::HTTP::Post.new(url.path) req.set_form_data(id: to_global_id, text: conte Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end
  28. Sum m ary Points for Practical metaprogramming Extract code from

    'meta' aspect & write code simply Define thin wrapper with reflection APIs. REVISED
  29. It's helpful to extract code from 'meta' aspect & code

    simply to avoid complicated code by over use of metaprogramming.
  30. But, I know it's so fun to use reflection APIs

    that I recommend to practice extreme metaprogramming :-)