-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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