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

Applying the Module Builder Pattern

Applying the Module Builder Pattern

If you haven't heard of this pattern, you're not alone! The Module Builder pattern is a recently-coined Ruby metaprogramming technique that extends Module in order to provide configuration for the behavior encapsulated within. Think of it like a template for generating Modules that are customized to the place you use them.

It's a powerful pattern that is used within Rails and the Mobility gem, but can also directly apply to your application code. This talk will walk through a real life example showing you how.

Presented at Columbus Ruby Brigade

Bradley Schaefer

August 21, 2017
Tweet

More Decks by Bradley Schaefer

Other Decks in Programming

Transcript

  1. class BodyShape attr_reader :api, :client NAME = 'New Womens Body

    Shape Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api = Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) end end
  2. class ItemTypeGroupPreferences attr_reader :api, :client NAME = 'New Womens Item

    Type Group Preferences' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api = Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) end end
  3. class PrimaryObjectives attr_reader :api, :client NAME = 'New Womens Primary

    Reasons Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api = Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) && client.created_at > Date.new(2017) end end
  4. class WomensBrands attr_reader :api, :client NAME = 'New Womens Brand

    Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api = Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) end end
  5. class WomensBrands include ABExperiment attr_reader :api, :client NAME = 'New

    Womens Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api = Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) end end
  6. class WomensBrands include ABExperiment attr_reader :api, :client NAME = 'New

    Womens Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def initialize(client) @api =Experiments::Api.new(client, NAME) @client = client end def in_experiment? api.in_cell?(TEST_CELL) end end
  7. module ABExperiment def initialize(client) @client = client @api = StitchFix::Experiments::API.new(client,

    NAME) end end Failure/Error: @api = Experiments::Api.new(client, NAME) NameError: uninitialized constant ABExperiment::NAME
  8. module ABExperiment def initialize(client) @client = client @api = Experiments::API.new(client,

    NAME) end end Failure/Error: @api = Experiments::Api.new(client, NAME) NameError: uninitialized constant ABExperiment::NAME
  9. module ABExperiment def initialize(client) @client = client @api = Experiments::API.new(client,

    NAME) end end Failure/Error: @api = Experiments::Api.new(client, NAME) NameError: uninitialized constant ABExperiment::NAME
  10. class WomensBrands include ABExperiment attr_reader :api, :client NAME = 'New

    Womens Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  11. class WomensBrands include ABExperiment attr_reader :api, :client NAME = 'New

    Womens Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  12. module ABExperiment attr_reader :api, :client def initialize(client) @client = client

    @api = Experiments::API.new( client, self.class.const_get(:NAME) ) end end
  13. class WomensBrands include ABExperiment attr_reader :api, :client NAME = 'New

    Womens Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  14. class WomensBrands include ABExperiment NAME = 'New Womens Brand Question'

    CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  15. class WomensBrands include ABExperiment NAME = 'New Womens Brand Question'

    CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  16. module ABExperiment extend ActiveSupport::Concern # ... module ClassMethods def define!

    Experiments::API.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end end
  17. module ABExperiment extend ActiveSupport::Concern # ... module ClassMethods def define!

    Experiments::API.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end end
  18. module ABExperiment extend ActiveSupport::Concern # ... module ClassMethods def define!

    Experiments::API.define_experiment do name const_get(:NAME) cell const_get(:CONTROL_CELL), control: true cell const_get(:TEST_CELL) end end end
  19. class WomensBrands include ABExperiment NAME = 'New Womens Brand Question'

    CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def self.define! Experiments::Api.define_experiment do name NAME cell CONTROL_CELL, control: true cell TEST_CELL end end def in_experiment? api.in_cell?(TEST_CELL) end end
  20. Experiments::API.define_experiment do name const_get(:NAME) cell const_get(:CONTROL_CELL), control: true cell const_get(:TEST_CELL)

    end Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for #<Experiments::DSL>
  21. Experiments::API.define_experiment do name const_get(:NAME) cell const_get(:CONTROL_CELL), control: true cell const_get(:TEST_CELL)

    end Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for #<Experiments::DSL>
  22. Experiments::API.define_experiment do name const_get(:NAME) cell const_get(:CONTROL_CELL), control: true cell const_get(:TEST_CELL)

    end Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for #<Experiments::DSL>
  23. class WomensBrands include ABExperiment NAME = 'New Womens Brand Question'

    CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def in_experiment? api.in_cell?(TEST_CELL) end end
  24. module ABExperiment extend ActiveSupport::Concern attr_reader :client, :api def initialize(client) @client

    = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end module ClassMethods def define! klass = self Experiments::API.define_experiment do name klass.const_get(:NAME) cell klass.const_get(:CONTROL_CELL), control: true cell klass.const_get(:TEST_CELL) end end end end
  25. class ABExperiment attr_reader :api, :client def initialize(client) @client = client

    @api = Experiments::API.new( self.class.const_get(:NAME), client ) end def self.define! klass = self Experiments::API.define_experiment do name klass.const_get(:NAME) cell klass.const_get(:CONTROL_CELL), control: true cell klass.const_get(:TEST_CELL) end end end
  26. class WomensBrands < ABExperiment include ABExperiment NAME = 'New Womens

    Brand Question' CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def in_experiment? api.in_cell?(TEST_CELL) end end
  27. class WomensBrands include ABExperiment NAME = 'New Womens Brand Question'

    CONTROL_CELL = 'Control' TEST_CELL = 'New Question' def in_experiment? api.in_cell?(TEST_CELL) end end
  28. class WomensBrands include ABExperimentBuilder.new( name: 'New Womens Brand Question' control:

    'Control' test: 'New Question' ) def in_experiment? api.in_cell?(TEST_CELL) end end
  29. ABExperiment = Module.new do # ... end include ABExperiment class

    ABExperimentBuilder < Module include ABExperimentBuilder.new(…)
  30. ABExperiment = Module.new do # ... end include ABExperiment class

    ABExperimentBuilder < Module include ABExperimentBuilder.new(…)
  31. class ABExperimentBuilder < Module def initialize(name:, control:, test:) attr_reader :api,

    :client const_set(:NAME, name) const_set(:CONTROL_CELL, control) const_set(:TEST_CELL, test) define_method(:initialize) do |client| @client = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end end end
  32. class ABExperimentBuilder < Module def initialize(name:, control:, test:) attr_reader :api,

    :client const_set(:NAME, name) const_set(:CONTROL_CELL, control) const_set(:TEST_CELL, test) define_method(:initialize) do |client| @client = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end end end
  33. class ABExperimentBuilder < Module def initialize(name:, control:, test:) attr_reader :api,

    :client const_set(:NAME, name) const_set(:CONTROL_CELL, control) const_set(:TEST_CELL, test) define_method(:initialize) do |client| @client = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end end end
  34. class ABExperimentBuilder < Module def initialize(name:, control:, test:) attr_reader :api,

    :client const_set(:NAME, name) const_set(:CONTROL_CELL, control) const_set(:TEST_CELL, test) define_method(:initialize) do |client| @client = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end end end
  35. class ABExperimentBuilder < Module # def initialize(…) module ClassMethods #

    ... same as before ... end def included(base) base.extend(ClassMethods) end end
  36. class WomensBrands include ABExperimentBuilder.new( name: 'New Womens Brand Question' control:

    'Control' test: 'New Question' ) def in_experiment? api.in_cell?(TEST_CELL) end end
  37. The Good The Bad Encapsulation Had to write a talk

    to explain the code Experiment class readability metaprogramming: bad Didn't change tests/interface Changing interface potentially valuable Create new experiments with less cut and paste code Builder class readability Ability to configure module is a powerful general idea
  38. class WomensBrands include ABExperiment ab_experiment( name: 'New Womens Brand Question'

    control: 'Control' test: 'New Question' ) def in_experiment? api.in_cell?(TEST_CELL) end end
  39. class ABExperimentBuilder < Module def initialize(name:, control:, test:) attr_reader :api,

    :client const_set(:NAME, name) const_set(:CONTROL_CELL, control) const_set(:TEST_CELL, test) define_method(:initialize) do |client| @client = client @api = Experiments::API.new( self.class.const_get(:NAME), client ) end end module ClassMethods def define! klass = self Experiments::API.define_experiment do name klass.const_get(:NAME) cell klass.const_get(:CONTROL_CELL), control: true cell klass.const_get(:TEST_CELL) end end end def included(base); base.extend(ClassMethods) end end