Slide 1

Slide 1 text

Applying the Module Builder Pattern [email protected] @soulcutter

Slide 2

Slide 2 text

Experiment Definitions

Slide 3

Slide 3 text

Control Cell Experiment Cell

Slide 4

Slide 4 text

Control Cell Experiment Cell LOSER LOSER

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

What stands out?

Slide 10

Slide 10 text

Duplication!

Slide 11

Slide 11 text

module ABExperiment def initialize(client) @client = client @api = Experiments::API.new(client, NAME) end end

Slide 12

Slide 12 text

module ABExperiment def initialize(client) @client = client @api = Experiments::API.new(client, NAME) end end

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Let's run the tests!

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

module ABExperiment def initialize(client) @client = client @api = Experiments::API.new( client, self.class.const_get(:NAME) ) end end

Slide 20

Slide 20 text

module ABExperiment def initialize(client) @client = client @api = Experiments::API.new( client, self.class::NAME ) end end

Slide 21

Slide 21 text

Tests Pass!

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

module ABExperiment attr_reader :api, :client def initialize(client) @client = client @api = Experiments::API.new( client, self.class.const_get(:NAME) ) end end

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Let's run the tests!

Slide 33

Slide 33 text

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 #

Slide 34

Slide 34 text

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 #

Slide 35

Slide 35 text

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 #

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Tests Pass!

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Why not a base class?

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Still have work to do!

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Module is a Class

Slide 47

Slide 47 text

ABExperiment = Module.new do # ... end module ABExperiment # ... end

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

class ABExperimentBuilder < Module # def initialize(…) module ClassMethods # ... same as before ... end def included(base) base.extend(ClassMethods) end end

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Let's talk tradeoffs

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Was this a good refactor?

Slide 59

Slide 59 text

When you might consider using this

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Shared behavior that needs configuration

Slide 62

Slide 62 text

Credits Chris Salzberg dejimata.com/2017/5/20/the-ruby-module-builder-pattern

Slide 63

Slide 63 text

Questions?

Slide 64

Slide 64 text

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