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

Practical Meta Programming on Rails Application

Practical Meta Programming on Rails Application

邦題: Railsアプリでの実用的メタプログラミング (lang:ja)

実プロジェクトで、やり過ぎにならずにメタプログラミングする方法を説明しました。

http://www.joho-shimane.or.jp/docs/2013092500011/

MOROHASHI Kyosuke

October 19, 2013
Tweet

More Decks by MOROHASHI Kyosuke

Other Decks in Programming

Transcript

  1. klasses = { 'array klass' => Array, 'hash klass' =>

    Hash, } def class_name(klass) klass.name end klasses['array klass'].new #=> [] class_name(klasses['hash klass']) #=> "Hash"
  2. # イテレータや処理の差し替え multiplier = ->(i) { i * 2 }

    square = ->(i) { i ** 2 } [1, 2, 3].map(&multiplier) [1, 2, 3].map(&square) # リソースの開閉 File.open(path, 'w') {|f| f.puts('content') } # 遅延評価 scope :fresh, -> { where('created_at > ?', 3.hour.ago) } ϒϩοΫ(Proc)
  3. 3VCZͷڭ͑ͯ͘Εͨ͜ͱ ౡాߒೋ [email protected] — You must unlearn what you have

    learned. ೥݄೔ ౔ ࡳຈ3VCZձٞ http://www.slideshare.net/snoozer05/20101204-youmustunlearnwhatyouhavelearned
  4. def up_or_down(condition) up_or_down = condition ? :upcase ? :downcase 'Ruby'.send(up_or_down)

    end up_or_down(true) # => 'RUBY' up_or_down(false) # => 'ruby' Object#send
  5. class Foo foo = 'FOO' define_method(:foo1) do puts foo +

    '1' end def foo2 puts foo + '2' end end obj = Foo.new obj.foo1 #=> 'FOO1' obj.foo2 #=> NameError define_method
  6. class Foo def initialize(message) @message = message end end Foo.class_eval

    %q[ def greet "#{@message} you!" end ] foo = Foo.new('hi') p foo.greet #=> 'hi you!' p foo.instance_eval { @message } #=> 'hi'
  7. class Foo def initialize(message) @message = message end end foo

    = Foo.new('hi') foo.instance_variable_get('@message') #=> 'hi'
  8. class Foo def method_missing(method, *args, &block) if method = :hi

    'hello' else super end end end Foo.new.hi #=> 'hello' method_missing
  9. module MyModule def self.included(base) "included by #{base}" end def self.extended(obj)

    "extended by #{obj}" end end class MyClass include MyModule # => "included by MyClass" end MyClass.new.extend(MyModule) # => "extended by #<MyClass:0x007fd included, extended
  10. module MyModule included(base) "included by #{base}" end module ClassMethods def

    hi; 'hello' ; end end end class MyClass include MyModule # => "included by MyClass" end MyClass.hi # => "hello" ActiveSupport::Concern
  11. 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 p Book.new(author: 'moro', price: '1,980')
  12. 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>
  13. 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(...) ͨ͘͞Μϝιου͕Ͱ͖Δ
  14. 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 end ೉ͦ͠͏ͳ࣮૷Πϝʔδ
  15. Associationͷநग़ ✓ owner ͱ target ͕͍Δ ✓ owner ͷ id

    ͱtarget ͷ fk Ͱݕࡧ ͢Δ scope Λ࡞Δ ✓ target Λϩʔυ͠ɺద੾ʹϝϞԽ ✓ ௥Ճ࡟আͷίʔϧόοΫΛ؅ཧ͢Δ
  16. = 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
  17. class Post < AR::Base after_save :submit_content_monitoring private def submit_content_monitoring content

    = [title, body].join("\n\n") url = Rails.config.censoring_endpoint req = Net::HTTP::Post.new(url.path) req.set_form_data('content' => content) Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end
  18. class Comment < AR::Base ... def submit_content_monitoring content = body

    ... end end class Photo < AR::Base ... def submit_content_monitoring content = "<img src='http://img.example.com/#{id}' />" ... end end
  19. class Post < AR::Base after_save :submit_content_monitoring private def submit_content_monitoring content

    = [title, body].join("\n\n") url = Rails.config.censoring_endpoint req = Net::HTTP::Post.new(url.path) req.set_form_data('content' => content) Net::HTTP.start(url.hostname, url.port) do |htt http.request(req) end end
  20. class Post < AR::Base after_save :submit_content_monitoring private def submit_content_monitoring Censoring.new(

    [title, body].join("\n\n"), Rails.config.censoring_endpoint ).submit end end
  21. class Censoring def initialize(content, url) @content = content @url =

    url end def submit req = Net::HTTP::Post.new(@url.path) req.set_form_data('content' => @content) Net::HTTP.start(@url.hostname, @url.port) do |h http.request(req) end end end
  22. class Post < AR::Base after_save :submit_content_monitoring private def submit_content_monitoring Censoring.new(

    [title, body].join("\n\n"), Rails.config.censoring_endpoint ).submit end end
  23. class CensorAdapter def initialize(endpoint, &block) @endpoint = endpoint @content_builder =

    block end def submit(record) content = @content_builder.call(record) Censoring.new(content, @endpoint).submit end end # ---------------- post_adapter = CensorAdapter.new(endpoint) do |post| [post.title, post.body].join("\n\n") end post_adapter.submit(post)
  24. class Post < AR::Base @@censor_adapter = CensorAdapter.new(config.endpoint) do |post| [post.title,

    post.body].join("\n\n") end after_save :submit_content_monitoring private def submit_content_monitoring @@censor_adapter.submit(self) end end
  25. class Comment < AR::Base @@censor_adapter = CensorAdapter.new(config.endpoint) do |comment| comment.body

    end after_save :submit_content_monitoring private def submit_content_monitoring @@censor_adapter.submit(self) end end
  26. class Photo < AR::Base @@censor_adapter = CensorAdapter.new(config.endpoint) do |photo| "<img

    src='http://img.example.com/#{photo.id}' />" end after_save :submit_content_monitoring private def submit_content_monitoring @@censor_adapter.submit(self) end end
  27. module Censorable extend ActiveSupport::Concern module ClassMethods def censor_content(url, &block) adapter

    = Censorable::Adapter.new(url, block after_save {|record| adapter.submit(record) end end class Adapter ... class Request ... end
  28. $ bundle gem censorable create censorable/Gemfile create censorable/Rakefile create censorable/LICENSE.txt

    create censorable/README.md create censorable/.gitignore create censorable/censorable.gemspec create censorable/lib/censorable.rb create censorable/lib/censorable/version.rb Initializating git repo in /Users/moro/tmp/censorab
  29. Gem::Specification.new do |spec| spec.name = "censorable" spec.version = Censorable::VERSION spec.authors

    = ["moro"] spec.email = ["[email protected]"] spec.description = %q{TODO: Write a gem descrip spec.summary = %q{TODO: Write a gem summary spec.homepage = "" spec.license = "MIT" spec.files = `git ls-files`.split($/) spec.executables = spec.files.grep(%r{^bin/}) { spec.test_files = spec.files.grep(%r{^(test|sp spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 1. spec.add_development_dependency "rake"
  30. $ bundle exec rake build censorable 0.0.1 built to pkg/censorable-0.0.1.gem.

    # rubygems.orgのアカウントがあればすぐ公開できる $ bundle exec rake release ͱͯ΋؆୯ʹgem͕Ͱ͖Δ
  31. $ bundle gem censorable create censorable/Gemfile create censorable/Rakefile create censorable/LICENSE.txt

    create censorable/README.md create censorable/.gitignore create censorable/censorable.gemspec create censorable/lib/censorable.rb create censorable/lib/censorable/version.rb Initializating git repo in /Users/moro/tmp/censorab ͋ͱ͸libͷԼʹ ಺༰Λ࣋ͬͯ͘Δ͚ͩ
  32. $ bundle gem censorable create censorable/Gemfile create censorable/Rakefile create censorable/LICENSE.txt

    create censorable/README.md create censorable/.gitignore create censorable/censorable.gemspec create censorable/lib/censorable.rb create censorable/lib/censorable/version.rb Initializating git repo in /Users/moro/tmp/censorab όʔδϣϯ͸͜ͷ ϑΝΠϧʹॻ͍͍ͯ͘
  33. gem 'censorable', path:'/Users/moro/tmp/censorable' or gem 'censorable', github: 'moro/censorable' or gem

    'censorable', git: 'git://github.com/moro/censo Gem leʹ௥Ճ͢Ε͹OK