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.

8a66c2a7197be751b21ebd35319ec797?s=128

Kevin Deisz

January 14, 2020
Tweet

Transcript

  1. Refinements in Ruby twitter.com/kddeisz

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

    end end
  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
  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
  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
  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
  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
  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
  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"
  10. Usage

  11. None
  12. Law of Demeter

  13. Law of Demeter Common interfaces

  14. Law of Demeter Common interfaces Local monkey-patches

  15. 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
  16. Law of Demeter

  17. Law of Demeter Only talk to your friends

  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' end end class Event < ApplicationRecord has_many :rsvps end
  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
  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
  21. 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
  22. 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
  23. 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
  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
  25. 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
  26. 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
  27. 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
  28. 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
  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
  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
  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
  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
  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
  34. 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
  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.name, event.type, event.starts_at, event.ends_at, event.sponsored, event.rvsps.size ] end end end end
  36. 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
  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
  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
  39. 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
  40. 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
  41. Common interfaces

  42. twitter.com/kddeisz Refinements in Ruby class User < ApplicationRecord def can_admin_organization?

    superuser? end end
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  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
  49. 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
  50. 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
  51. Local monkey-patches

  52. 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
  53. 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
  54. 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
  55. 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
  56. twitter.com/kddeisz Refinements in Ruby class Rsvp < ApplicationRecord before_validation do

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

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

    before_validation do self.message = message.blank? ? nil : message end end
  59. Conclusions

  60. Code that changes together should live together

  61. Refinements can move code out of class definitions and into

    where it is used
  62. Use refinements to organize your code so developers don’t have

    to open so many files to make changes
  63. Refinements in Ruby twitter.com/kddeisz