Ruby Ate My DSL!

Ruby Ate My DSL!

Presentation by Daniel Azuma at RubyConf 2019. Covers how DSLs can clash with "normal" Ruby code, and techniques for hardening a DSL against such conflicts.

E06aa8f63d2a1753a2b352bc1cabbde2?s=128

Daniel Azuma

November 18, 2019
Tweet

Transcript

  1. 2.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  2. 3.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  3. 4.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  4. 5.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  5. 6.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  6. 7.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  7. 11.
  8. 13.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  9. 14.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  10. 15.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end method added to the “main” object
  11. 16.
  12. 17.

    # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end
  13. 18.

    # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper
  14. 19.

    # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper uses instance_eval to set self to a mapper instance
  15. 20.

    DSL howto: • Add methods to an existing object •

    Change “self” within a block
  16. 23.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  17. 24.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block
  18. 25.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block spec
  19. 26.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb
  20. 27.

    # nanospec.rb module Kernel def describe name, &block end end

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  21. 28.

    # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  22. 29.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end
  23. 30.

    # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  24. 31.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  25. 32.

    # nanospec.rb class Entity def initialize name @name = name

    end def it name, &block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  26. 33.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  27. 34.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  28. 35.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  29. 36.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end
  30. 37.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  31. 38.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  32. 39.

    # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  33. 40.

    DSL

  34. 42.

    TIP: Use a naming convention for instance variables and private

    methods # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end
  35. 43.

    # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  36. 44.

    # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def __check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  37. 47.

    TIP: # nanospec.rb class Entity def initialize name @name =

    name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end both the DSL and its implementation Delegate implementation to a separate object
  38. 48.

    # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  39. 49.

    TIP: class EntityDSL def initialize impl @__impl = impl end

    def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only # nanospec.rb class EntityImpl def initialize name @name = name @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only Delegate implementation to a separate object
  40. 50.

    # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Delegate implementation to a separate object
  41. 51.

    # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  42. 52.

    DSL

  43. 53.
  44. 54.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  45. 55.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  46. 56.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  47. 57.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end TIP: Add methods only where needed
  48. 58.
  49. 60.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  50. 62.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  51. 63.

    private method of Object # nanospec_test.rb require "./nanospec.rb" describe "foo"

    do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  52. 64.

    # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... subclass of Object # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  53. 65.

    # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end BasicObject?
  54. 66.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end FAIL # nanospec.rb class Entity < BasicObject def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... BasicObject?
  55. 67.

    # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end TIP: BasicObject is usually NOT an effective base class for a DSL
  56. 68.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  57. 69.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end
  58. 70.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping
  59. 71.

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping method of singleton class block executed with instance_eval
  60. 72.

    TIP: Design DSLs to honor lexical scoping # nanospec_test.rb require

    "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end def try_speaking puts "testing 1...2...3..." end end describe "bar" do it "speaks" do try_speaking end def try_speaking puts "I'm a different object." end end separate scopes
  61. 73.

    # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping nested block
  62. 74.

    # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping delegate methods
  63. 75.

    # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  64. 76.

    Each “describe” block creates a class # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  65. 77.

    Each test is executed within a separate instance # nanospec_test.rb

    require "./nanospec.rb" describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  66. 78.

    # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class TIP: Consider modeling blocks as classes
  67. 79.

    # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class Use class_eval instead of instance_eval TIP: Consider modeling blocks as classes
  68. 80.

    TIP: Consider modeling blocks as classes # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  69. 81.
  70. 82.

    ≈ ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE

    = "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end
  71. 83.

    ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE =

    "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end Constant redefined warning (both constants defined on Object)
  72. 85.

    TIP: Provide alternatives to constants # rspec_example.rb require "rspec" describe

    "foo" do let(:message) { "Hello, world!" } it "speaks" do puts message end end describe "bar" do let(:message) { "Hello, RubyConf!" } it "speaks" do puts message end end
  73. 86.
  74. 87.

    DSL

  75. 88.

    Use a naming convention for instance variables and private methods

    Delegate implementation to a separate object DSL
  76. 89.
  77. 90.

    Add methods only where needed BasicObject is usually NOT an

    effective base class for a DSL Design DSLs to honor lexical scoping Consider modeling blocks as classes ❤ DSL
  78. 91.

    DSL