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

A Brief Introduction to AttributeMethods

A Brief Introduction to AttributeMethods

If you've ever created a Ruby on Rails application, you've used AttributeMethods. These two modules -- one in ActiveModel, another in ActiveRecord -- are included in every model in your application, their methods called every time you read or write to an attribute. Given their importance, you would think there would be a lot of information about them out there, but actually there's very little.

https://kichijojikaigi.doorkeeper.jp/events/66983
https://github.com/rails/rails/pull/30895

Chris Salzberg

November 10, 2017
Tweet

More Decks by Chris Salzberg

Other Decks in Programming

Transcript

  1. KichijojiKaigi #12
    A Brief Introduction to
    A Brief Introduction to
    AttributeMethods
    AttributeMethods
    by Chris Salzberg

    View Slide

  2. きっかけ

    View Slide

  3. View Slide

  4. Model Ancestry
    Topic.ancestors
    => [Topic(...),
    Topic::GeneratedAssociationMethods,
    #,
    ActiveRecord::Base,
    ActiveRecord::Suppressor,
    ActiveRecord::SecureToken,
    ActiveRecord::Store,
    ActiveRecord::Serialization,
    ActiveModel::Serializers::JSON,
    ActiveModel::Serialization,
    ActiveRecord::Reflection,
    ActiveRecord::NoTouching,
    ActiveRecord::TouchLater,
    ...
    Topic.ancestors
    => [Topic(...),
    Topic::GeneratedAssociationMethods,
    #,
    ActiveRecord::Base,
    ActiveRecord::Suppressor,
    ActiveRecord::SecureToken,
    ActiveRecord::Store,
    ActiveRecord::Serialization,
    ActiveModel::Serializers::JSON,
    ActiveModel::Serialization,
    ActiveRecord::Reflection,
    ActiveRecord::NoTouching,
    ActiveRecord::TouchLater,
    ...
    class Topic < ActiveRecord::Base
    # ...
    end
    class Topic < ActiveRecord::Base
    # ...
    end

    View Slide

  5. Model Ancestry
    Topic.ancestors
    => [Topic(...),
    Topic::GeneratedAssociationMethods,
    #,
    ActiveRecord::Base,
    ActiveRecord::Base,
    ActiveRecord::Suppressor,
    ActiveRecord::SecureToken,
    ActiveRecord::Store,
    ActiveRecord::Serialization,
    ActiveModel::Serializers::JSON,
    ActiveModel::Serialization,
    ActiveRecord::Reflection,
    ActiveRecord::NoTouching,
    ActiveRecord::TouchLater,
    ...
    Topic.ancestors
    => [Topic(...),
    Topic::GeneratedAssociationMethods,
    #,
    ActiveRecord::Base,
    ActiveRecord::Base,
    ActiveRecord::Suppressor,
    ActiveRecord::SecureToken,
    ActiveRecord::Store,
    ActiveRecord::Serialization,
    ActiveModel::Serializers::JSON,
    ActiveModel::Serialization,
    ActiveRecord::Reflection,
    ActiveRecord::NoTouching,
    ActiveRecord::TouchLater,
    ...
    class Topic < ActiveRecord::Base
    # ...
    end
    class Topic < ActiveRecord::Base
    # ...
    end
    What’s this?

    View Slide

  6. Generated Attribute Methods
    generated_methods = Topic.ancestors[2]
    => #
    generated_methods.instance_methods(false)
    => []
    Topic.new
    => #
    generated_methods.instance_methods(false)
    => [:type=,
    :content,
    :title_before_type_cast,
    :title_came_from_user?,
    :title_changed?,
    :title_change,
    :title_will_change!,
    :title_was,
    :title_previously_changed?,
    :title_previous_change,
    :restore_title!,
    :saved_change_to_title?,
    :saved_change_to_title,
    :title_before_last_save,
    :will_save_change_to_title?,
    :title_change_to_be_saved,
    :title_in_database,
    ... ]
    generated_methods = Topic.ancestors[2]
    => #
    generated_methods.instance_methods(false)
    => []
    Topic.new
    => #
    generated_methods.instance_methods(false)
    => [:type=,
    :content,
    :title_before_type_cast,
    :title_came_from_user?,
    :title_changed?,
    :title_change,
    :title_will_change!,
    :title_was,
    :title_previously_changed?,
    :title_previous_change,
    :restore_title!,
    :saved_change_to_title?,
    :saved_change_to_title,
    :title_before_last_save,
    :will_save_change_to_title?,
    :title_change_to_be_saved,
    :title_in_database,
    ... ]

    View Slide

  7. Generated Attribute Methods
    generated_methods = Topic.ancestors[2]
    => #
    generated_methods.instance_methods(false)
    => []
    Topic.new
    => #
    generated_methods.instance_methods(false)
    => [:type=,
    :content,
    :title_before_type_cast,
    :title_came_from_user?,
    :title_changed?,
    :title_change,
    :title_will_change!,
    :title_was,
    :title_previously_changed?,
    :title_previous_change,
    :restore_title!,
    :saved_change_to_title?,
    :saved_change_to_title,
    :title_before_last_save,
    :will_save_change_to_title?,
    :title_change_to_be_saved,
    :title_in_database,
    ... ]
    generated_methods = Topic.ancestors[2]
    => #
    generated_methods.instance_methods(false)
    => []
    Topic.new
    => #
    generated_methods.instance_methods(false)
    => [:type=,
    :content,
    :title_before_type_cast,
    :title_came_from_user?,
    :title_changed?,
    :title_change,
    :title_will_change!,
    :title_was,
    :title_previously_changed?,
    :title_previous_change,
    :restore_title!,
    :saved_change_to_title?,
    :saved_change_to_title,
    :title_before_last_save,
    :will_save_change_to_title?,
    :title_change_to_be_saved,
    :title_in_database,
    ... ]
    ?

    View Slide

  8. My PR
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...

    View Slide

  9. My PR
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    ActiveRecord::AttributeMethods::Dirty

    View Slide

  10. My PR
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    ActiveRecord::AttributeMethods::Dirty
    ActiveModel::Dirty

    View Slide

  11. My PR
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    ActiveRecord::AttributeMethods::Dirty
    ActiveModel::Dirty
    ActiveRecord::AttributeMethods::Query
    ActiveRecord::AttributeMethods::BeforeTypeCast

    View Slide

  12. AttributeMethods: Rails has Two

    ActiveModel::AttributeMethods
    – Virtual attributes
    – Prefixed/suffixed methods map to handlers

    ActiveRecord::AttributeMethods
    – Persisted attributes
    – Uses prefixes/suffixes to define:

    reader/writer/query methods

    (persisted) dirty attributes

    type casting methods

    View Slide

  13. ActiveModel
    ActiveModel
    ::
    ::
    AttributeMethods
    AttributeMethods

    View Slide

  14. View Slide

  15. Provides a way to add prefixes and suffixes
    to your methods as well as handling the
    creation of ActiveRecord::Base-like class
    methods such as table_name.
    Summary

    View Slide

  16. Provides a way to add prefixes and suffixes
    to your methods as well as handling the
    creation of ActiveRecord::Base-like class
    methods such as table_name.
    Summary

    View Slide

  17. Usage

    include ActiveModel::AttributeMethods in your class

    Call each of its methods you want to add, such as
    attribute_method_suffix or attribute_method_prefix.

    Call define_attributes after the other methods are called.

    Define the various generic _attribute methods that you have
    declared (“handlers”)

    Define an attributes method which returns a hash with each
    attribute name in your model as hash key and the attribute value
    as hash value. Hash keys must be strings.

    View Slide

  18. class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'

    View Slide

  19. class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    Include the Module

    View Slide

  20. class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    Define prefixes/suffixes

    View Slide

  21. class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    Define Handlers

    View Slide

  22. Handling Attribute Methods

    Define prefix/suffix/affix and corresponding
    handlers which receive attribute name
    attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
    attribute_method_suffix "_previously_changed?", "_previous_change"
    attribute_method_affix prefix: "restore_", suffix: "!"
    attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
    attribute_method_suffix "_previously_changed?", "_previous_change"
    attribute_method_affix prefix: "restore_", suffix: "!"
    def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
    !!changes_include?(attr) &&
    (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
    (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
    end
    def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
    !!changes_include?(attr) &&
    (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
    (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
    end

    View Slide

  23. def attribute_method_prefix(*prefixes)
    self.attribute_method_matchers += prefixes.map! do |prefix|
    AttributeMethodMatcher.new prefix: prefix
    end
    undefine_attribute_methods
    end
    def attribute_method_prefix(*prefixes)
    self.attribute_method_matchers += prefixes.map! do |prefix|
    AttributeMethodMatcher.new prefix: prefix
    end
    undefine_attribute_methods
    end
    def attribute_method_suffix(*suffixes)
    self.attribute_method_matchers += suffixes.map! do |suffix|
    AttributeMethodMatcher.new suffix: suffix
    end
    undefine_attribute_methods
    end
    def attribute_method_suffix(*suffixes)
    self.attribute_method_matchers += suffixes.map! do |suffix|
    AttributeMethodMatcher.new suffix: suffix
    end
    undefine_attribute_methods
    end
    def attribute_method_affix(*affixes)
    self.attribute_method_matchers += affixes.map! do |affix|
    AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix]
    end
    undefine_attribute_methods
    end
    def attribute_method_affix(*affixes)
    self.attribute_method_matchers += affixes.map! do |affix|
    AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix]
    end
    undefine_attribute_methods
    end
    Prefixes/Suffixes/Affixes

    View Slide

  24. Attribute Method Matcher
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end

    View Slide

  25. Attribute Method Matcher
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end

    View Slide

  26. Attribute Method Matcher
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end

    View Slide

  27. Attribute Method Matcher
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end

    View Slide

  28. Attribute Method Matcher
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end
    class AttributeMethodMatcher
    attr_reader :prefix, :suffix, :method_missing_target
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    end
    def match(method_name)
    if @regex =~ method_name
    AttributeMethodMatch.new(method_missing_target, $1, method_name)
    end
    end
    def method_name(attr_name)
    @method_name % attr_name
    end
    def plain?
    prefix.empty? && suffix.empty?
    end
    end

    View Slide

  29. “Method Missing Target”
    "#{@prefix}attribute#{@suffix}"

    View Slide

  30. “Method Missing”?
    # Allows access to the object attributes, which are held in the hash
    # returned by attributes, as though they were first-class
    # methods. So a +Person+ class with a +name+ attribute can for example use
    # Person#name and Person#name= and never directly use
    # the attributes hash -- except for multiple assignments with
    # ActiveRecord::Base#attributes=.
    #
    # It's also possible to instantiate related objects, so a Client
    # class belonging to the +clients+ table with a +master_id+ foreign key
    # can instantiate master through Client#master.
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    # Allows access to the object attributes, which are held in the hash
    # returned by attributes, as though they were first-class
    # methods. So a +Person+ class with a +name+ attribute can for example use
    # Person#name and Person#name= and never directly use
    # the attributes hash -- except for multiple assignments with
    # ActiveRecord::Base#attributes=.
    #
    # It's also possible to instantiate related objects, so a Client
    # class belonging to the +clients+ table with a +master_id+ foreign key
    # can instantiate master through Client#master.
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end

    View Slide

  31. “Method Missing”?
    #
    #
    #
    #
    #
    #
    #
    # It's also possible to instantiate related objects, so a Client
    # class belonging to the +clients+ table with a +master_id+ foreign key
    # can instantiate master through Client#master.
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    #
    #
    #
    #
    #
    #
    #
    # It's also possible to instantiate related objects, so a Client
    # class belonging to the +clients+ table with a +master_id+ foreign key
    # can instantiate master through Client#master.
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    # Allows access to the object attributes, which are held in the hash
    # returned by attributes, as though they were first-class
    # methods. So a +Person+ class with a +name+ attribute can for example use
    # Person#name and Person#name= and never directly use
    # the attributes hash -- except for multiple assignments with
    # ActiveRecord::Base#attributes=.

    View Slide

  32. Method Missing Matching
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  33. Method Missing Matching
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  34. Method Missing Matching
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  35. Method Missing Matching
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  36. “Attribute Method”?
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  37. attribute_method?
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end

    View Slide

  38. attribute_method?
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attributes
    { 'name' => @name }
    end
    def attributes
    { 'name' => @name }
    end

    View Slide

  39. attribute_method?
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attributes
    { 'name' => @name }
    end
    def attributes
    { 'name' => @name }
    end

    Returns true if the attribute name is a key on
    the model’s attributes hash

    View Slide

  40. Attribute Missing?
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def method_missing(method, *args, &block)
    if respond_to_without_attributes?(method, true)
    super
    else
    match = matched_attribute_method(method.to_s)
    match ? attribute_missing(match, *args, &block) : super
    end
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def matched_attribute_method(method_name)
    matches = self.class.send(:attribute_method_matchers_matching, method_name)
    matches.detect { |match| attribute_method?(match.attr_name) }
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end
    def self.attribute_method_matchers_matching(method_name)
    attribute_method_matchers_cache.compute_if_absent(method_name) do
    # Must try to match prefixes/suffixes first, or else the matcher with no
    # prefix/suffix will match every time.
    matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
    matchers.map { |method| method.match(method_name) }.compact
    end
    end

    View Slide

  41. attribute_missing
    # +attribute_missing+ is like +method_missing+, but for attributes. When
    # +method_missing+ is called we check to see if there is a matching
    # attribute method. If so, we tell +attribute_missing+ to dispatch the
    # attribute. This method can be overloaded to customize the behavior.
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end
    # +attribute_missing+ is like +method_missing+, but for attributes. When
    # +method_missing+ is called we check to see if there is a matching
    # attribute method. If so, we tell +attribute_missing+ to dispatch the
    # attribute. This method can be overloaded to customize the behavior.
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end

    View Slide

  42. attribute_missing
    #
    #
    #
    #
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end
    #
    #
    #
    #
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end
    # +attribute_missing+ is like +method_missing+, but for attributes. When
    # +method_missing+ is called we check to see if there is a matching
    # attribute method. If so, we tell +attribute_missing+ to dispatch the
    # attribute. This method can be overloaded to customize the behavior.

    attribute_missing is method_missing,
    but for attributes
    – if reset_foo_to_default! matches prefix/suffix
    matchers, attribute_missing is called with the
    match target, the attribute, and any args/block

    View Slide

  43. attribute_missing
    # +attribute_missing+ is like +method_missing+, but for attributes. When
    # +method_missing+ is called we check to see if there is a matching
    # attribute method. If so, we tell +attribute_missing+ to dispatch the
    # attribute. This method can be overloaded to customize the behavior.
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end
    # +attribute_missing+ is like +method_missing+, but for attributes. When
    # +method_missing+ is called we check to see if there is a matching
    # attribute method. If so, we tell +attribute_missing+ to dispatch the
    # attribute. This method can be overloaded to customize the behavior.
    def attribute_missing(match, *args, &block)
    __send__(match.target, match.attr_name, *args, &block)
    end
    @prefix @suffix attribute
    name
    reset_attribute_to_default!(“name”)
    reset_attribute_to_default!(“name”)

    View Slide

  44. Method Missing Flow
    person.reset_name_to_default!
    person.reset_name_to_default!

    View Slide

  45. Method Missing Flow
    person.reset_name_to_default!
    person.reset_name_to_default!
    Which matchers have matches?
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]

    View Slide

  46. Method Missing Flow
    person.reset_name_to_default!
    person.reset_name_to_default!
    Which matchers have matches?
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
    Which are included in attributes?
    { ‘name’ => @name }.include?(“name”)
    { ‘name’ => @name }.include?(“name”)

    View Slide

  47. Method Missing Flow
    person.reset_name_to_default!
    person.reset_name_to_default!
    Which matchers have matches?
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
    matchers.map { |method| method.match(method_name) }.compact
    #=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
    Which are included in attributes?
    { ‘name’ => @name }.include?(“name”)
    { ‘name’ => @name }.include?(“name”)
    attribute_missing calls handler method
    person.reset_attribute_to_default!(“name”)
    person.reset_attribute_to_default!(“name”)

    View Slide

  48. But...

    View Slide

  49. Rails almost never actually does this.

    View Slide

  50. class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    class Person
    include ActiveModel::AttributeMethods
    attribute_method_affix prefix: 'reset_',
    suffix: '_to_default!'
    attribute_method_suffix '_contrived?'
    attribute_method_prefix 'clear_'
    define_attribute_methods :name
    attr_accessor :name
    def attributes
    { 'name' => @name }
    end
    private
    def attribute_contrived?(attr)
    true
    end
    def clear_attribute(attr)
    send("#{attr}=", nil)
    end
    def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
    end
    end
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    person = Person.new
    person.name
    # => 'Gem'
    person.name_contrived?
    # => true
    person.clear_name
    person.name
    # => nil
    person.reset_name_to_default!
    person.name
    # => 'Default Name'
    Define Attribute Methods

    View Slide

  51. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    Defining Attribute Methods

    View Slide

  52. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    Defining Attribute Methods

    View Slide

  53. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    Defining Attribute Methods
    For AR
    (Ugly!)

    View Slide

  54. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    Defining Attribute Methods
    Defines mapping to handler
    (like method_missing)

    View Slide

  55. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    Defining Attribute Methods
    What’s this?

    View Slide

  56. def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    Defining Attribute Methods
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    Here too

    View Slide

  57. def generated_attribute_methods
    @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
    end
    def generated_attribute_methods
    @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
    end
    Tap, Include & Memoize

    Common technique:
    – Instantiate anonymous module (Module.new)
    – include module into class
    – memoize included module

    Now dynamically define methods on
    module, and they will be overridable in class

    View Slide

  58. AM::AttributeMethods Recap

    Defines prefixed/suffixed matchers

    Overrides method_missing (respond_to?)
    to dispatch to attribute methods

    For performance, for each matcher also
    defines methods on included anonymous
    module to dispatch to attribute methods

    (Also can be used to define aliases, similar to
    generated methods but on class)

    View Slide

  59. ActiveRecord
    ActiveRecord
    ::
    ::
    AttributeMethods
    AttributeMethods

    View Slide

  60. View Slide

  61. Some Differences

    Assumes attributes are column values

    Assumes including class is an AR::Base

    Changes to make it threadsafe

    Blacklists certain method names

    Overrides respond_to?, hardcodes
    assumption that attributes are columns

    View Slide

  62. # frozen_string_literal: true
    require "mutex_m"
    module ActiveRecord
    # = Active Record Attribute Methods
    module AttributeMethods
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods
    included do
    initialize_generated_modules
    include Read
    include Write
    include BeforeTypeCast
    include Query
    include PrimaryKey
    include TimeZoneConversion
    include Dirty
    include Serialization
    # frozen_string_literal: true
    require "mutex_m"
    module ActiveRecord
    # = Active Record Attribute Methods
    module AttributeMethods
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods
    included do
    initialize_generated_modules
    include Read
    include Write
    include BeforeTypeCast
    include Query
    include PrimaryKey
    include TimeZoneConversion
    include Dirty
    include Serialization
    Extending AM:AttributeMethods
    Include module in module
    ?

    View Slide

  63. Initialize Generated Modules
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end

    View Slide

  64. Initialize Generated Modules
    class GeneratedAttributeMethods < Module
    include Mutex_m
    end
    class GeneratedAttributeMethods < Module
    include Mutex_m
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    A Module Builder!

    View Slide

  65. Initialize Generated Modules
    class GeneratedAttributeMethods < Module
    include Mutex_m
    end
    class GeneratedAttributeMethods < Module
    include Mutex_m
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    Module Builder instance acts like a mutex

    View Slide

  66. def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    def initialize_generated_modules
    @generated_attribute_methods = GeneratedAttributeMethods.new
    @attribute_methods_generated = false
    include @generated_attribute_methods
    super
    end
    Eagerly Generate Modules
    module ActiveRecord
    module AttributeMethods
    # ...
    module ClassMethods
    def inherited(child_class)
    child_class.initialize_generated_modules
    super
    end
    module ActiveRecord
    module AttributeMethods
    # ...
    module ClassMethods
    def inherited(child_class)
    child_class.initialize_generated_modules
    super
    end
    module ActiveRecord
    module AttributeMethods
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods
    included do
    initialize_generated_modules
    module ActiveRecord
    module AttributeMethods
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods
    included do
    initialize_generated_modules

    View Slide

  67. Why You No Memoize?
    to avoid check-then-set race conditions

    View Slide

  68. def generated_attribute_methods
    @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
    end
    def generated_attribute_methods
    @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
    end
    Race Condition

    Two (or more) threads could instantiate, include
    and return two (or more) different modules

    AR avoids this by eagerly assigning
    @generated_attribute_methods when
    including AttributeMethods or when
    subclassing AR model class

    View Slide

  69. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods

    View Slide

  70. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.

    View Slide

  71. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.

    Since generated_attribute_methods is an
    instance of a class (Module subclass) which
    includes Mutex_m, we can call synchronize on it

    View Slide

  72. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods
    def attribute_names
    @attribute_names ||= if !abstract_class? && table_exists?
    attribute_types.keys
    else
    []
    end
    end
    def attribute_names
    @attribute_names ||= if !abstract_class? && table_exists?
    attribute_types.keys
    else
    []
    end
    end

    View Slide

  73. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods
    ?

    View Slide

  74. # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    # Generates all the attribute related methods for columns in the database
    # accessors, mutators and query methods.
    def define_attribute_methods # :nodoc:
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    generated_attribute_methods.synchronize do
    return false if @attribute_methods_generated
    superclass.define_attribute_methods unless self == base_class
    super(attribute_names)
    @attribute_methods_generated = true
    end
    end
    define_attribute_methods
    ?

    When attribute methods are defined on an AR
    model, all superclasses up to AR::Base also
    get their attribute methods defined

    View Slide

  75. Initializing an AR Object

    View Slide

  76. def initialize(attributes = nil)
    self.class.define_attribute_methods
    @attributes = self.class._default_attributes.deep_dup
    init_internals
    initialize_internals_callback
    # ...
    def initialize(attributes = nil)
    self.class.define_attribute_methods
    @attributes = self.class._default_attributes.deep_dup
    init_internals
    initialize_internals_callback
    # ...
    Eagerly Defining Methods
    module ActiveRecord
    module Core
    # ...
    module ClassMethods
    def allocate
    define_attribute_methods
    super
    end
    module ActiveRecord
    module Core
    # ...
    module ClassMethods
    def allocate
    define_attribute_methods
    super
    end
    module ActiveRecord
    module Core
    # ...
    def init_with(coder)
    coder = LegacyYamlAdapter.convert(self.class, coder)
    @attributes = self.class.yaml_encoder.decode(coder)
    init_internals
    @new_record = coder["new_record"]
    self.class.define_attribute_methods
    module ActiveRecord
    module Core
    # ...
    def init_with(coder)
    coder = LegacyYamlAdapter.convert(self.class, coder)
    @attributes = self.class.yaml_encoder.decode(coder)
    init_internals
    @new_record = coder["new_record"]
    self.class.define_attribute_methods

    View Slide

  77. attribute_method?
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    ActiveModel::AttributeMethods
    def attribute_method?(attribute)
    super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
    end
    def attribute_method?(attribute)
    super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
    end
    ActiveRecord::AttributeMethods

    View Slide

  78. attribute_method?
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    def attribute_method?(attr_name)
    respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
    end
    ActiveModel::AttributeMethods
    def attribute_method?(attribute)
    super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
    end
    def attribute_method?(attribute)
    super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
    end
    ActiveRecord::AttributeMethods
    Private Method? Yuck.

    View Slide

  79. def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    instance_method_
    already_implemented?
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end

    View Slide

  80. def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    instance_method_
    already_implemented?
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    Another Private Method?

    View Slide

  81. def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    def instance_method_already_implemented?(method_name)
    if dangerous_attribute_method?(method_name)
    raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
    to make sure that you don't have an attribute or method with the same name."
    end
    if superclass == Base
    super
    else
    # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
    # defines its own attribute method, then we don't want to overwrite that.
    defined = method_defined_within?(method_name, superclass, Base) &&
    ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
    defined || super
    end
    end
    instance_method_
    already_implemented?
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    def instance_method_already_implemented?(method_name)
    generated_attribute_methods.method_defined?(method_name)
    end
    ?

    View Slide

  82. def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    respond_to?

    View Slide

  83. def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    respond_to?

    View Slide

  84. def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    def respond_to?(name, include_private = false)
    return false unless super
    case name
    when :to_partial_path
    name = "to_partial_path".freeze
    when :to_model
    name = "to_model".freeze
    else
    name = name.to_s
    end
    # If the result is true then check for the select case.
    # For queries selecting a subset of columns, return false for unselected columns.
    # We check defined?(@attributes) not to issue warnings if called on objects that
    # have been allocated but not yet initialized.
    if defined?(@attributes) && self.class.column_names.include?(name)
    return has_attribute?(name)
    end
    true
    end
    respond_to?
    Coupled
    to ActiveRecord

    View Slide

  85. def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_methods(*attr_names)
    attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    def define_attribute_method(attr_name)
    attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)
    unless instance_method_already_implemented?(method_name)
    generate_method = "define_method_#{matcher.method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, generated_attribute_methods, method_name,
    matcher.method_missing_target, attr_name.to_s
    end
    end
    end
    attribute_method_matchers_cache.clear
    end
    More AM/AR Coupling
    ActiveModel::AttributeMethods

    View Slide

  86. def define_method_attribute(name)
    safe_name = name.unpack("h*".freeze).first
    temp_method = "__temp__#{safe_name}"
    ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
    sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
    generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
    def #{temp_method}
    #{sync_with_transaction_state}
    name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
    _read_attribute(name) { |n| missing_attribute(n, caller) }
    end
    STR
    generated_attribute_methods.module_eval do
    alias_method name, temp_method
    undef_method temp_method
    end
    end
    def define_method_attribute(name)
    safe_name = name.unpack("h*".freeze).first
    temp_method = "__temp__#{safe_name}"
    ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
    sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
    generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
    def #{temp_method}
    #{sync_with_transaction_state}
    name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
    _read_attribute(name) { |n| missing_attribute(n, caller) }
    end
    STR
    generated_attribute_methods.module_eval do
    alias_method name, temp_method
    undef_method temp_method
    end
    end
    ActiveRecord::AttrMethods::Read
    "define_method_#{"attribute"}"

    View Slide

  87. def define_method_attribute=(name)
    safe_name = name.unpack("h*".freeze).first
    ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
    sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
    generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
    def __temp__#{safe_name}=(value)
    name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
    #{sync_with_transaction_state}
    _write_attribute(name, value)
    end
    alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
    undef_method :__temp__#{safe_name}=
    STR
    end
    def define_method_attribute=(name)
    safe_name = name.unpack("h*".freeze).first
    ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
    sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
    generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
    def __temp__#{safe_name}=(value)
    name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
    #{sync_with_transaction_state}
    _write_attribute(name, value)
    end
    alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
    undef_method :__temp__#{safe_name}=
    STR
    end
    ActiveRecord::AttrMethods::Write
    "define_method_#{"attribute="}"

    View Slide

  88. Problems

    Hard to extend
    – AM/AR AttributeMethods heavily coupled
    – Can’t use pure AM prefix/suffixes in AR model

    Poor encapsulation
    – Many private method overrides

    Confusing and obscure internal API
    – define_attribute_method
    – define_method_attribute

    View Slide

  89. NASTY!

    View Slide

  90. Ok, you do better

    Extract AttributeMethodMatcher into a
    Module Builder :
    – prefix/suffix + generated regex
    – methods for defining attribute methods on itself
    – dynamically defined method_missing + respond_to?

    Each prefix/suffix/affix has its own module

    ActiveRecord module builder can subclass
    ActiveModel module builder

    View Slide

  91. module ActiveModel
    class AttributeMethodMatcher < Module
    NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
    CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
    attr_reader :prefix, :suffix, :method_missing_target, :method_names
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    @method_names = Set.new
    define_method_missing
    end
    # ...
    module ActiveModel
    class AttributeMethodMatcher < Module
    NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
    CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
    attr_reader :prefix, :suffix, :method_missing_target, :method_names
    AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
    def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
    @method_names = Set.new
    define_method_missing
    end
    # ...
    Module Builder Refactor (1)

    View Slide

  92. def define_method_missing
    matcher = self
    define_method :method_missing do |method_name, *arguments, &method_block|
    if (match = matcher.match(method_name.to_s)) &&
    method_name != :attributes &&
    attribute_method?(match.attr_name) &&
    !respond_to_without_attributes?(method_name, true)
    attribute_missing(match, *arguments, &method_block)
    else
    super(method_name, *arguments, &method_block)
    end
    end
    end
    def define_method_missing
    matcher = self
    define_method :method_missing do |method_name, *arguments, &method_block|
    if (match = matcher.match(method_name.to_s)) &&
    method_name != :attributes &&
    attribute_method?(match.attr_name) &&
    !respond_to_without_attributes?(method_name, true)
    attribute_missing(match, *arguments, &method_block)
    else
    super(method_name, *arguments, &method_block)
    end
    end
    end
    Module Builder Refactor (2)

    Access builder method using a closure

    View Slide

  93. def define_attribute_method(attr_name)
    name = method_name(attr_name)
    method_names << name.to_sym
    unless instance_method_already_implemented?(name)
    generate_method = "define_method_#{method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, name, method_missing_target, attr_name.to_s
    end
    end
    end
    def define_attribute_method(attr_name)
    name = method_name(attr_name)
    method_names << name.to_sym
    unless instance_method_already_implemented?(name)
    generate_method = "define_method_#{method_missing_target}"
    if respond_to?(generate_method, true)
    send(generate_method, attr_name.to_s)
    else
    define_proxy_call true, name, method_missing_target, attr_name.to_s
    end
    end
    end
    Module Builder Refactor (3)

    Builder defines attribute methods on itself, no
    passing around anonymous module

    View Slide

  94. module ActiveRecord
    class AttributeMethodMatcher < ActiveModel::AttributeMethodMatcher
    include Mutex_m
    def initialize(*)
    super
    @attribute_methods_generated = false
    end
    #...
    def define_attribute_methods(*attr_names)
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    synchronize do
    return false if @attribute_methods_generated
    super(*attr_names)
    @attribute_methods_generated = true
    end
    end
    # ...
    module ActiveRecord
    class AttributeMethodMatcher < ActiveModel::AttributeMethodMatcher
    include Mutex_m
    def initialize(*)
    super
    @attribute_methods_generated = false
    end
    #...
    def define_attribute_methods(*attr_names)
    return false if @attribute_methods_generated
    # Use a mutex; we don't want two threads simultaneously trying to define
    # attribute methods.
    synchronize do
    return false if @attribute_methods_generated
    super(*attr_names)
    @attribute_methods_generated = true
    end
    end
    # ...
    AR subclasses AM

    View Slide

  95. The Result
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...
    Topic.ancestors
    => [Topic(id: integer, title: string, author_name: string...),
    Topic::GeneratedAssociationMethods,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ActiveRecord::Base,
    ...

    View Slide

  96. AR decoupled from AM

    AR and AM matchers are entirely isolated

    Can co-exist alongside each other in ancestors

    Can use AM::AttributeMethods in AR::Base
    model

    Subclass AR::AttributeMethodMatcher to
    add new attribute features and use in any
    AR::Base model

    View Slide

  97. Module Bloat?

    View Slide

  98. Alternative Implementation

    Module Builder “lite” version using just one
    builder
    – All matchers in one builder instance
    – Delegate attribute_method_prefix, etc to
    module

    Retains benefits of code encapsulation

    Avoids “module bloat”, all methods on one
    module

    View Slide

  99. Alternative Implementation

    Module Builder “lite” version using just one
    builder
    – All matchers in one builder instance
    – Delegate attribute_method_prefix, etc to
    module

    Retains benefits of code encapsulation

    Avoids “module bloat”, all methods on one
    module

    But still struggling with mysterious 4-5% performance drop...

    View Slide

  100. View Slide