Skip to content

Instantly share code, notes, and snippets.

@danrabinowitz
Forked from searls/market_research.rb
Created October 7, 2016 20:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danrabinowitz/67c1e79878009889243688af5e5ce3a5 to your computer and use it in GitHub Desktop.
Save danrabinowitz/67c1e79878009889243688af5e5ce3a5 to your computer and use it in GitHub Desktop.
Was chatting with @mfeathers about retaining Ruby's chained Enumerable style, but finding a way to inject names that reflects the application domain (as opposed to just littering functional operations everywhere, which may be seen as a sort of Primitive Obsession)
# A little toy file demonstrating how to build chainable
# data transformations that reveal some amount of intent
# through named extracted methods.
#
# Kudos to @mfeathers for giving me the idea to try this
#
# Copyright Test Double, LLC, 2016. All Rights Reserved.
require_relative "marketing_refinements"
require_relative "research_subjects"
class MarketResearch
# Vanilla / Anonymous / Primitive approach to chaining
def income_by_smoking(data)
Hash[
data.reject {|p| p[:income] < 10_000 }.
group_by { |p| p[:smoker] }.
map { |(is_smoker, people)|
[
is_smoker ? :smokers : :non_smokers,
people.map {|p| p[:income]}.reduce(:+).to_f / people.size
]
}
]
end
# Refined approach to tacking named domain abstractions onto Array/Hash
using MarketingRefinements
def income_by_smoking_fancy(data)
data.exclude_incomes_under(10_000).
separate_people_by(:smoker).
average_income_by_smoking
end
# Using a dedicated class for research_subjects
def income_by_smoking_with_custom_class(data)
ResearchSubjects.new(data)
.exclude_incomes_under(10_000)
.separate_people_by(:smoker)
.average_income_by_smoking
end
end
DATA = [
{age: 19, smoker: false, income: 10_000, education: :high_school},
{age: 49, smoker: true, income: 120_000, education: :bachelors},
{age: 55, smoker: false, income: 400_000, education: :masters},
{age: 23, smoker: true, income: 10_000, education: :bachelors},
{age: 70, smoker: false, income: 70_000, education: :phd },
{age: 34, smoker: false, income: 90_000, education: :masters},
{age: 90, smoker: true, income: 0, education: :high_school},
]
original_result = MarketResearch.new.income_by_smoking(DATA)
fancy_result = MarketResearch.new.income_by_smoking_fancy(DATA)
wrapped_array_result = MarketResearch.new.income_by_smoking_with_custom_class(DATA)
puts <<-MSG
Original result: #{original_result}
Fancy result: #{fancy_result}
wrapped_array_result: #{wrapped_array_result}
MSG
module MarketingRefinements
refine Array do
# Domain-specific
def exclude_incomes_under(min)
reject {|p| p[:income] < min }
end
def separate_people_by(attribute)
group_by { |p| p[attribute] }
end
# General-purpose
def average(attr)
map {|el| el[attr]}.reduce(:+).to_f / size
end
end
refine Hash do
# Domain-specific
def average_income_by_smoking
Hash[
transform_keys { |is_smoker|
is_smoker ? :smokers : :non_smokers
}.map {|key, people|
[key, people.average(:income)]
}
]
end
# General-purpose
def transform_keys
{}.tap do |result|
self.each_key do |key|
result[yield(key)] = self[key]
end
end
end
end
end
require 'forwardable'
module WrappedArray
extend Forwardable
TYPE_PRESERVING_METHODS = %i(reduce reject group_by each_key [])
def initialize(items)
@items = items
end
def_delegators :@items, :size, :to_s, :map
TYPE_PRESERVING_METHODS.each do |method_name|
define_method(method_name) do |*args, &block|
self.class.new(
@items.send(method_name, *args, &block)
)
end
end
end
class ResearchSubjects
include WrappedArray
def exclude_incomes_under(min)
reject {|p| p[:income] < min }
end
def separate_people_by(attribute)
group_by { |p| p[attribute] }
end
def average_income_by_smoking
Hash[
transform_keys { |is_smoker|
is_smoker ? :smokers : :non_smokers
}.map {|key, people|
[key, people.average(:income)]
}
]
end
def transform_keys
{}.tap do |result|
self.each_key do |key|
result[yield(key)] = self[key]
end
end
end
def average(attr)
map {|el| el[attr]}.reduce(:+).to_f / size
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment