Slide 1

Slide 1 text

ABExperimentBuilder Applying the Module Builder Pattern Bradley.Schaefer@stitchfix.com @soulcutter

Slide 2

Slide 2 text

A/B Experiments The Way Things Were

Slide 3

Slide 3 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 4

Slide 4 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 5

Slide 5 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 6

Slide 6 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 7

Slide 7 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 8

Slide 8 text

What Stands Out?

Slide 9

Slide 9 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 10

Slide 10 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 11

Slide 11 text

Removing Duplication def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end

Slide 12

Slide 12 text

Removing Duplication module ABExperiment def initialize(client) @api = StitchFix::Experiments::API.new(experiment_name: NAME, test_subject: client) @client = client end end

Slide 13

Slide 13 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 14

Slide 14 text

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 allocation Time.now..6.months.from_now live true owner Client.first 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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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…

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Scope? Credit: Craig McMillan 2008 https://mccraigmccraig.wordpress.com/2008/10/29/ruby-objects-classes-and-eigenclasses/

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 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

Slide 24

Slide 24 text

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 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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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 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

Slide 27

Slide 27 text

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 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

Slide 28

Slide 28 text

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 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

Slide 29

Slide 29 text

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 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

Slide 30

Slide 30 text

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 allocation Time.now..6.months.from_now live true owner Client.first end end end end

Slide 31

Slide 31 text

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 allocation Time.now..6.months.from_now live true owner Client.first end end end end

Slide 32

Slide 32 text

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) allocation Time.now..6.months.from_now live true owner Client.first end end end end

Slide 33

Slide 33 text

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) allocation Time.now..6.months.from_now live true owner Client.first end end end end Let's run the tests…

Slide 34

Slide 34 text

This is new… Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for #

Slide 35

Slide 35 text

Not the expected scope Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for # def define! StitchFix::Experiments::API.define_experiment do name const_get(:NAME) treatment const_get(:CONTROL_CELL), control: true treatment const_get(:TEST_CELL) allocation Time.now..6.months.from_now live true owner Client.first end end

Slide 36

Slide 36 text

Let's fix that Failure/Error: name const_get(:NAME) NoMethodError: undefined method `const_get' for # 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

Slide 37

Slide 37 text

module ABExperiment extend ActiveSupport::Concern included do attr_reader :client, :api end 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) allocation Time.now..6.months.from_now live true owner Client.first end end end end

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

But Wait, There's MORE!

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Wouldn't this be nice? 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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

We did it!

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Extending Module? class ABExperimentBuilder < Module

Slide 47

Slide 47 text

Module is a Class module ABExperiment # … end ABExperiment = Module.new do # … end Module.class # => Class

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

ABExperimentBuilder builds Modules 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

Slide 50

Slide 50 text

ABExperimentBuilder builds Modules 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

Slide 51

Slide 51 text

ABExperimentBuilder builds Modules 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

Slide 52

Slide 52 text

ABExperimentBuilder builds Modules 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

Slide 53

Slide 53 text

ABExperimentBuilder builds Modules 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' )

Slide 54

Slide 54 text

This is what a built module would look like 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' ) {

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

You don't need a Concern module ClassMethods # … end def included(base) base.extend ClassMethods end

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Questions?

Slide 62

Slide 62 text

ABExperimentBuilder Applying the Module Builder Pattern Bradley.Schaefer@stitchfix.com @soulcutter