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

Refinements in Ruby

Refinements in Ruby

In Ruby 2.0, refinements were introduced as a language-level feature. Since then they haven't seen wide-spread adoption in the community; it's time to change that! We'll walk through what refinements are, how they work, and what problems they can solve. Come see better ways to organize your spaghetti code without sacrificing readability.

Kevin Newton

January 14, 2020
Tweet

More Decks by Kevin Newton

Other Decks in Programming

Transcript

  1. Refinements in Ruby
    twitter.com/kddeisz

    View full-size slide

  2. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end

    View full-size slide

  3. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end

    View full-size slide

  4. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end

    View full-size slide

  5. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end

    View full-size slide

  6. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end
    using M
    c = C.new

    View full-size slide

  7. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end
    using M
    c = C.new

    View full-size slide

  8. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end
    using M
    c = C.new

    View full-size slide

  9. twitter.com/kddeisz
    Refinements in Ruby
    class C
    def foo
    puts "C#foo"
    end
    end
    module M
    refine C do
    def foo
    puts "C#foo in M"
    end
    end
    end
    using M
    c = C.new
    c.foo # prints "C#foo in M"

    View full-size slide

  10. Law of Demeter

    View full-size slide

  11. Law of Demeter

    Common interfaces

    View full-size slide

  12. Law of Demeter

    Common interfaces

    Local monkey-patches

    View full-size slide

  13. twitter.com/kddeisz
    Refinements in Ruby
    class Organization < ApplicationRecord
    has_many :events
    has_many :users
    end
    class Event < ApplicationRecord
    belongs_to :host, class_name: 'User'
    belongs_to :organization
    has_many :rsvps
    end
    class Rsvp < ApplicationRecord
    belongs_to :event
    belongs_to :user
    end
    class User < ApplicationRecord
    belongs_to :organization
    has_many :hosted_events,
    foreign_key: :host_id,
    class_name: 'Event'
    has_many :rsvps
    end

    View full-size slide

  14. Law of Demeter

    View full-size slide

  15. Law of Demeter
    Only talk to
    your friends

    View full-size slide

  16. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    !event.invite_only? || rsvp.status_was == 'invited'
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  17. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    (!event.invite_only? || rsvp.status_was == 'invited') &&
    (event.cap? && event.rsvps.size <= event.cap - 1)
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  18. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    (!event.invite_only? || rsvp.status_was == 'invited') &&
    (event.cap? && event.rsvps.size <= event.cap - 1)
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  19. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    (!event.invite_only? || rsvp.status_was == 'invited') &&
    (event.cap? && event.rsvps.size <= event.cap - 1)
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  20. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    (!event.invite_only? || rsvp.status_was == 'invited') &&
    (event.cap? && event.rsvps.size <= event.cap - 1)
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  21. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate :allowed_by_event?
    private
    def allowed_by_event?
    event.allowed_rsvp?(status_was)
    end
    end
    class Event < ApplicationRecord
    has_many :rsvps
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end

    View full-size slide

  22. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate { event.allowed_rsvp?(status_was) }
    end
    class Event < ApplicationRecord
    has_many :rsvps
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end

    View full-size slide

  23. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    using Module.new {
    refine Event do
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end
    }
    belongs_to :event
    validate { event.allowed_rsvp?(status_was) }
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  24. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate { event.allowed_rsvp?(status_was) }
    end
    class Event < ApplicationRecord
    has_many :rsvps
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end

    View full-size slide

  25. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    belongs_to :event
    validate { event.allowed_rsvp?(status_was) }
    end
    class Event < ApplicationRecord
    has_many :rsvps
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end

    View full-size slide

  26. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    using Module.new {
    refine Event do
    def allowed_rsvp?(status_was)
    (!invite_only? || status_was == 'invited') &&
    (cap? && rsvps.size <= cap - 1)
    end
    end
    }
    belongs_to :event
    validate { event.allowed_rsvp?(status_was) }
    end
    class Event < ApplicationRecord
    has_many :rsvps
    end

    View full-size slide

  27. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  28. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  29. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  30. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  31. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  32. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  33. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end
    end
    end

    View full-size slide

  34. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << to_row(event)
    end
    end
    end
    private
    def to_row(event)
    [
    event.name,
    event.type,
    event.starts_at,
    event.ends_at,
    event.sponsored,
    event.rvsps.size
    ]
    end
    end

    View full-size slide

  35. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << event.to_row
    end
    end
    end
    end
    class Event < ApplicationRecord
    def to_row
    [
    name,
    type,
    starts_at,
    ends_at,
    sponsored,
    rvsps.size
    ]
    end
    end

    View full-size slide

  36. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    using Module.new {
    refine Event do
    def to_row
    [
    name,
    type,
    starts_at,
    ends_at,
    sponsored,
    rvsps.size
    ]
    end
    end
    }
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << event.to_row
    end
    end
    end
    end

    View full-size slide

  37. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << event.to_row
    end
    end
    end
    end
    class Event < ApplicationRecord
    def to_row
    [
    name,
    type,
    starts_at,
    ends_at,
    sponsored,
    rvsps.size
    ]
    end
    end

    View full-size slide

  38. twitter.com/kddeisz
    Refinements in Ruby
    class EventExportJob < ApplicationJob
    using Module.new {
    refine Event do
    def to_row
    [
    name,
    type,
    starts_at,
    ends_at,
    sponsored,
    rvsps.size
    ]
    end
    end
    }
    def perform(organization)
    Export.generate(organization) do |export|
    organization.events.each do |event|
    export << event.to_row
    end
    end
    end
    end

    View full-size slide

  39. Common interfaces

    View full-size slide

  40. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    def can_admin_organization?
    superuser?
    end
    end

    View full-size slide

  41. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    def can_admin_event?(event)
    event.host == self
    end
    def can_admin_organization?
    superuser?
    end
    end

    View full-size slide

  42. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    def can_admin_event?(event)
    event.host == self
    end
    def can_admin_organization?
    superuser?
    end
    def can_admin_rsvp?(rsvp)
    rsvp.user == self || can_admin_event?(rsvp.event)
    end
    end

    View full-size slide

  43. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    def can_admin?(record)
    case record
    when Event
    record.host == self
    when Organization
    superuser?
    when Rsvp
    record.user == self || can_admin?(record.event)
    else
    raise ArgumentError
    end
    end
    end

    View full-size slide

  44. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    def can_admin?(record)
    record.admin?(self)
    end
    end
    class Event < ApplicationRecord
    def admin?(user)
    host == user
    end
    end
    class Organization < ApplicationRecord
    def admin?(user)
    user.superuser?
    end
    end
    class Rsvp < ApplicationRecord
    def admin?(other)
    user == other || event.admin?(other)
    end
    end

    View full-size slide

  45. twitter.com/kddeisz
    Refinements in Ruby
    class User < ApplicationRecord
    using Module.new {
    refine Event do
    def admin?(user)
    host == user
    end
    end
    refine Organization do
    def admin?(user)
    user.superuser?
    end
    end
    refine Rsvp do
    def admin?(other)
    user == other || event.admin?(other)
    end
    end
    }
    def can_admin?(record)
    record.admin?(self)
    ennd

    View full-size slide

  46. twitter.com/kddeisz
    Refinements in Ruby
    class AdminRules < Struct.new(:user)
    using Module.new {
    refine Event do
    def admin?(user)
    host == user
    end
    end
    refine Organization do
    def admin?(user)
    user.superuser?
    end
    end
    refine Rsvp do
    def admin?(other)
    user == other || event.admin?(other)
    end
    end
    }
    def can_admin?(record)
    record.admin?(user)
    ennd

    View full-size slide

  47. twitter.com/kddeisz
    Refinements in Ruby
    class AdminRules < Struct.new(:user)
    using Module.new {
    refine Event do
    def admin?(user)
    host == user
    end
    end
    refine Organization do
    def admin?(user)
    user.superuser?
    end
    end
    refine Rsvp do
    def admin?(other)
    user == other || event.admin?(other)
    end
    end
    }
    def can_admin?(record)
    record.admin?(user)
    ennd
    class User < ApplicationRecord
    delegate :can_admin?,
    to: :admin_rules
    def admin_rules
    AdminRules.new(self)
    end
    end

    View full-size slide

  48. twitter.com/kddeisz
    Refinements in Ruby
    class AdminRules < Struct.new(:user)
    using Module.new {
    refine Event do
    def admin?(user)
    host == user
    end
    end
    refine Organization do
    def admin?(user)
    user.superuser?
    end
    end
    refine Rsvp do
    def admin?(other)
    user == other || event.admin?(other)
    end
    end
    }
    def can_admin?(record)
    record.admin?(user)
    ennd
    class User < ApplicationRecord
    delegate :can_admin?,
    to: :admin_rules
    def admin_rules
    AdminRules.new(self)
    end
    end

    View full-size slide

  49. Local
    monkey-patches

    View full-size slide

  50. twitter.com/kddeisz
    Refinements in Ruby
    class Hash
    def except(*keys)
    slice(*self.keys - keys)
    end
    end
    class UsersController < ApplicationController
    def update
    if @user.update(params[:user].except(:admin))
    render @user, status: :success
    else
    render @user.errors, status: :unprocessable_entity
    end
    end
    end

    View full-size slide

  51. twitter.com/kddeisz
    Refinements in Ruby
    class UsersController < ApplicationController
    using Module.new {
    refine Hash do
    def except(*keys)
    slice(*self.keys - keys)
    end
    end
    }
    def update
    if @user.update(params[:user].except(:admin))
    render @user, status: :success
    else
    render @user.errors, status: :unprocessable_entity
    end
    end
    end

    View full-size slide

  52. twitter.com/kddeisz
    Refinements in Ruby
    class Object
    def blank?
    respond_to?(:empty?) ? !!empty? : !self
    end
    end
    class NilClass
    def blank?
    true
    end
    end
    class FalseClass
    def blank?
    true
    end
    end
    class TrueClass
    def blank?
    false
    end
    end
    class Array
    alias_method :blank?, :empty?
    end
    class Hash
    alias_method :blank?, :empty?
    end
    class String
    def blank?
    empty? || BLANK_RE.match?(self)
    end
    end

    View full-size slide

  53. twitter.com/kddeisz
    Refinements in Ruby
    module Blank
    refine Object do
    def blank?
    respond_to?(:empty?) ? !!empty? : !self
    end
    end
    refine NilClass do
    def blank?
    true
    end
    end
    refine FalseClass do
    def blank?
    true
    end
    end
    refine TrueClass do
    def blank?
    false
    end
    ennd
    module Blank
    refine Array do
    alias_method :blank?, :empty?
    end
    refine Hash do
    alias_method :blank?, :empty?
    end
    refine String do
    def blank?
    empty? || BLANK_RE.match?(self)
    end
    end
    end

    View full-size slide

  54. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    before_validation do
    self.message = message.blank? ? nil : message
    end
    end

    View full-size slide

  55. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    using Blank
    before_validation do
    self.message = message.blank? ? nil : message
    end
    end

    View full-size slide

  56. twitter.com/kddeisz
    Refinements in Ruby
    class Rsvp < ApplicationRecord
    using ActiveSupport::StringExtensions
    before_validation do
    self.message = message.blank? ? nil : message
    end
    end

    View full-size slide

  57. Code that changes
    together should live
    together

    View full-size slide

  58. Refinements can move
    code out of class definitions
    and into where it is used

    View full-size slide

  59. Use refinements to organize your
    code so developers don’t have to
    open so many files to make changes

    View full-size slide

  60. Refinements in Ruby
    twitter.com/kddeisz

    View full-size slide