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

Applying the Module Builder Pattern

Applying the Module Builder Pattern

Slides given on 2017-08-03 at Cleveland Ruby Brigade

A walkthrough of a refactor in Ruby illustrating how to apply the Module Builder pattern. Also introduces some basic metaprogramming techniques that are more widely-applicable.

Much appreciation to Chris Salzberg who coined the pattern and discusses it in-depth here: http://dejimata.com/2017/5/20/the-ruby-module-builder-pattern

Bradley Schaefer

August 03, 2017
Tweet

More Decks by Bradley Schaefer

Other Decks in Programming

Transcript

  1. A/B Experiments The Way Things Were class BodyShape attr_reader :api,

    :client NAME = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  2. A/B Experiments The Way Things Were class ItemTypeGroupPreferences attr_reader :api,

    :client NAME = 'New Womens Item Type Group Preferences' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_new_womens_item_type_group_preferences_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  3. A/B Experiments The Way Things Were class PrimaryObjectives attr_reader :api,

    :client NAME = 'New Womens Primary Reasons Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_primary_objectives_question? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  4. A/B Experiments The Way Things Were class WomensBrands attr_reader :api,

    :client NAME = 'New Womens Brands Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_womens_brands_question? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  5. A/B Experiments The Way Things Were class BottomsAttributePreferences attr_reader :api,

    :client NAME = 'New Womens Bottoms Attribute Preferences' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_new_bottoms_attribute_preferences_question? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  6. What Stands Out? class BodyShape attr_reader :api, :client NAME =

    'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) @client = client end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end class PrimaryObjectives attr_reader :api, :client NAME = 'New Womens Primary Reasons Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) @client = client end def show_primary_objectives_question? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  7. Duplication! class BodyShape attr_reader :api, :client NAME = 'New Womens

    Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) @client = client end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end class PrimaryObjectives attr_reader :api, :client NAME = 'New Womens Primary Reasons Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) @client = client end def show_primary_objectives_question? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  8. Removing Duplication class BodyShape attr_reader :api, :client NAME = 'New

    Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  9. Replace all initializers class BodyShape include ABExperiment attr_reader :api, :client

    NAME = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  10. Looking better! class BodyShape include ABExperiment attr_reader :api, :client NAME

    = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL allocation Time.now..6.months.from_now live true owner Client.first end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  11. But wait… there's a problem module ABExperiment def initialize(client) @api

    = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end
  12. But wait… there's a problem module ABExperiment def initialize(client) @api

    = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end Let's run the tests…
  13. But wait… there's a problem module ABExperiment def initialize(client) @api

    = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end Failure/Error: @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) NameError: uninitialized constant ABExperiment::NAME
  14. But wait… there's a problem module ABExperiment def initialize(client) @api

    = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end Need a way to look up a constant in the correct scope
  15. But wait… there's a problem module ABExperiment def initialize(client) @api

    = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end Failure/Error: @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) NameError: uninitialized constant ABExperiment::NAME
  16. const_get does the trick module ABExperiment def initialize(client) @api =

    StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) @client = client end end
  17. What stands out now? class BodyShape include ABExperiment attr_reader :api,

    :client NAME = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  18. What stands out now? class BodyShape include ABExperiment attr_reader :api,

    :client NAME = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  19. Encapsulate api/client module ABExperiment attr_reader :api, :client def initialize(client) @api

    = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) @client = client end end
  20. Encapsulate api/client class BodyShape include ABExperiment attr_reader :api, :client NAME

    = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  21. Looking even better! class BodyShape include ABExperiment NAME = 'New

    Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  22. What stands out now? class BodyShape include ABExperiment NAME =

    'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  23. What stands out now? class BodyShape include ABExperiment NAME =

    'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  24. We can extract that too! module ABExperiment extend ActiveSupport::Concern #

    ... module ClassMethods def define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end end end
  25. Oh yeah, I've seen this before module ABExperiment extend ActiveSupport::Concern

    # ... module ClassMethods def define! StitchFix::Experiments::API.define_experiment do name NAME treatment CONTROL_CELL, control: true treatment TEST_CELL end end end end
  26. Oh yeah, I've seen this before module ABExperiment extend ActiveSupport::Concern

    # ... module ClassMethods def define! StitchFix::Experiments::API.define_experiment do name const_get(:NAME) treatment const_get(:CONTROL_CELL), control: true treatment const_get(:TEST_CELL) end end end end
  27. Oh yeah, I've seen this before module ABExperiment extend ActiveSupport::Concern

    # ... module ClassMethods def define! StitchFix::Experiments::API.define_experiment do name const_get(:NAME) treatment const_get(:CONTROL_CELL), control: true treatment const_get(:TEST_CELL) end end end end Let's run the tests…
  28. Not the expected scope Failure/Error: name const_get(:NAME) NoMethodError: undefined method

    `const_get' for #<StitchFix::Experiments::DSL: 0x007fc103dc8e28> def define! StitchFix::Experiments::API.define_experiment do name const_get(:NAME) treatment const_get(:CONTROL_CELL), control: true treatment const_get(:TEST_CELL) end end
  29. Let's fix that Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get'

    for #<StitchFix::Experiments::DSL: 0x007fc103dc8e28> def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) end end
  30. module ABExperiment extend ActiveSupport::Concern attr_reader :client, :api def initialize(client) @client

    = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end module ClassMethods def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) end end end end
  31. Looking really good! class BodyShape include ABExperiment NAME = 'New

    Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  32. There's something still bothering me class BodyShape include ABExperiment NAME

    = 'New Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  33. An Encapsulation Problem class BodyShape include ABExperiment NAME = 'New

    Womens Body Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  34. The Module Builder Pattern! class BodyShape include ABExperimentBuilder.new( name: 'New

    Womens Body Shape Question', control_cell: 'Control', test_cell: 'New Question' ) def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  35. class ABExperimentBuilder < Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME,

    name) const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end module ClassMethods def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) end end end def included(base) base.extend ClassMethods end end
  36. class ABExperimentBuilder < Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME,

    name) const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end module ClassMethods def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) end end end def included(base) base.extend ClassMethods end end
  37. class ABExperimentBuilder < Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME,

    name) const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end module ClassMethods def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) allocation Time.now..6.months.from_now live true owner Client.first end end end def included(base) base.extend ClassMethods end end
  38. Configuring a Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME, name)

    const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end
  39. Configuring a Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME, name)

    const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end
  40. Configuring a Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME, name)

    const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end
  41. Configuring a Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME, name)

    const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end
  42. Configuring a Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME, name)

    const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end ABExperimentBuilder.new( name: 'New Womens Body Shape Question', test_cell: 'New Question' )
  43. Template for Building Modules module ABExperimentTemplate NAME = 'Foo' CONTROL_CELL

    = 'Control' TEST_CELL = 'Test' attr_reader :api, :client def initialize(client) @client = client @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) end end ABExperimentBuilder.new( name: 'Foo', test_cell: 'Test', control_cell: 'Control' ) {
  44. Template for Building Modules module ABExperimentTemplate NAME = 'Foo' CONTROL_CELL

    = 'Control' TEST_CELL = 'Test' attr_reader :api, :client def initialize(client) @client = client @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) end end ABExperimentBuilder.new( name: 'Foo', test_cell: 'Test', control_cell: 'Control' ) {
  45. Template for Building Modules module ABExperimentTemplate NAME = 'Foo' CONTROL_CELL

    = 'Control' TEST_CELL = 'Test' attr_reader :api, :client def initialize(client) @client = client @api = StitchFix::Experiments::API.new( experiment_name: NAME, test_subject: client ) end end ABExperimentBuilder.new( name: 'Foo', test_cell: 'Test', control_cell: 'Control' ) {
  46. class ABExperimentBuilder < Module def initialize(name:, test_cell:, control_cell: 'Control') const_set(:NAME,

    name) const_set(:CONTROL_CELL, control_cell) const_set(:TEST_CELL, test_cell) attr_reader :api, :client define_method(:initialize) do |client| @client = client @api = StitchFix::Experiments::API.new( experiment_name: self.class.const_get(:NAME), test_subject: client ) end end module ClassMethods def define! klass = self StitchFix::Experiments::API.define_experiment do name klass.const_get(:NAME) treatment klass.const_get(:CONTROL_CELL), control: true treatment klass.const_get(:TEST_CELL) allocation Time.now..6.months.from_now live true owner Client.first end end end def included(base) base.extend ClassMethods end end
  47. You don't need a Concern module ClassMethods # … end

    def included(base) base.extend ClassMethods end
  48. The Final Result class BodyShape include ABExperimentBuilder.new( name: 'New Womens

    Body Shape Question', control_cell: 'Control', test_cell: 'New Question' ) def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  49. The Interesting Bits, Front and Center class BodyShape include ABExperimentBuilder.new(

    name: 'New Womens Body Shape Question', control_cell: 'Control', test_cell: 'New Question' ) def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  50. Module Builder Tradeoffs Good Bad Experiment Class Readability I wrote

    a talk to explain the code in ABExperimentBuilder Can test module builder code and be confident it works across all experiments Too much magic? It's a neat Ruby technique metaprogramming: bad Didn't change tests/interface Changing interface potentially valuable
  51. Configuring via a method class BodyShape include ABExperiment ab_experiment( name:

    'New Womens Body Shape Question', control_cell: 'Control', test_cell: 'New Question' ) def show_new_body_shape_questions? api.allocating?(time: client.create_date) && api.in_cell?(cell_name: TEST_CELL) end end
  52. Config Method Tradeoffs Good Bad A more-familiar-looking pattern? Just as

    much if not more metaprogramming Have to jump through hoops to NOT define methods directly on the class If you define methods on the class, no ability to override behavior via super I didn't write a talk about that