文字内容
1. Sprinkles of Functional Programming johnschoeman
2. Hello, I'm John I work at johnschoeman
3. Github /johnschoeman Mastadon johnschoeman@technology.social Twitter @john_at_aol_dot_com_at_gmail_dot_com johnschoeman
4. Roadmap Thesis FP and OO A Recommendation A Story with Some Code Recap / Action Item Questions johnschoeman
5. Thesis johnschoeman
6. Ruby is a general purpose language johnschoeman
7. class A < B Object.new define_method(:foo) -> (x) { x x + 1 } data.map yield_self / then johnschoeman
8. Different paradigms lend themselves to different tasks johnschoeman
9. OO -> Behavior FP -> Data johnschoeman
10. We should choose our programming style based off the task at hand johnschoeman
11. If you are modeling behavior, prefer classes and composition. If you are handling data, prefer data pipelines and folds. johnschoeman
12. FP and OO johnschoeman
13. Objects and functions are a useful distinction. johnschoeman
14. What is difficult to change is perhaps a better distinction. johnschoeman
15. New Method New Data OO Existing code unchanged Existing code changed FP Existing code changed Existing code unchanged johnschoeman
16. OO Typical Cases • Resource modeling • Behaviour modeling • State modeling johnschoeman
17. FP Typical Cases • Data transformations • Data aggregates • Data streaming johnschoeman
18. A Recommendation johnschoeman
19. Given that methods are easy to add in OO and data is easy to add in FP johnschoeman
20. Ask how you expect requirements will change before beginning a task johnschoeman
21. Base this question off the context of your business and what users might want johnschoeman
22. Choose your style based off of answer to this question johnschoeman
23. A Story with some code johnschoeman
24. Imagine a Rails App johnschoeman
25. johnschoeman
26. Initial Requirement Allow users to upload a csv johnschoeman
27. Initial Requirement johnschoeman
28. Commit 0 Allow users to upload csv of products johnschoeman
29. RSpec.describe "User uploads a file of produce data" do scenario "by visiting the root page and clicking 'upload file'" do visit root_path attach_file("file", Rails.root.join("spec/fixtures/products.csv")) click_on "Upload File" expect(Product.count).to eq 3 end end johnschoeman
30. class ImportsController < ApplicationController def create Product.import(params[:file].path) redirect_to products_path, notice: "Succesfully imported" end end johnschoeman
31. describe ".import" do context "the file is a csv" do it "saves every row in the file as new product" do filepath = stub_csv Product.import(filepath) expect(Product.count).to eq 3 end end end johnschoeman
32. class Product < ApplicationRecord validates :name, presence: true ... def self.import(file_path) CSV.foreach(file_path, headers: true) do row data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.zone.parse(data[:release_date]) create(data) end end end johnschoeman
33. - app - controllers imports_controller.rb - models - product.rb johnschoeman
34. - spec - fixtures - products.csv - features - user_uploads_a_products_file_spec.rb - models - product_spec.rb johnschoeman
35. Initial Requirement Gif johnschoeman
36. johnschoeman
37. New Requirement csv + xlsx johnschoeman
38. Commit 1 Allow users to upload csv of products Introduce product data importer johnschoeman
39. RSpec.describe ProductDataImporter it "saves every row in the file as new product" do filepath = stub_csv importer = ProductDataImporter.new(filepath) importer.import expect(Product.count).to eq 3 end end johnschoeman
40. class ProductDataImporter attr_reader :filepath def initialize(filepath) @filepath = filepath end def import CSV.foreach(filepath, headers: true) do row data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.zone.parse(data[:release_date]) Product.create(data) end end end johnschoeman
41. class ImportsController < ApplicationController def create importer = ProductDataImporter.new(params[:file].path) importer.import redirect_to products_path, notice: "Succesfully imported" end end johnschoeman
42. Commit 2 Allow users to upload csv of products Introduce product data importer Introduce product data formatter johnschoeman
43. Rspec.describe ProductDataFormatter it "builds a data hash from a csv_row for the product" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] csv_row = CSV::Row.new(headers, fields) formatter = ProductDataFormatter.new result = formatter.build(csv_row) expect(result).to eq(expected_results) end end johnschoeman
44. class ProductDataFormatter def build(csv_row) data = csv_row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.zone.parse(data[:release_date]) data end end johnschoeman
45. Commit 3 Allow users to upload csv of products Introduce product data importer Introduce product data formatter Allow .xlsx format for importer johnschoeman
46. Rspec.describe ProductDataImporter context "the file is a xlsx" do it "saves every row in the file as new product" do filename = "products.xlsx" stub_xlsx(filename) formatter = ProductDataFormatter.new importer = ProductDataImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
47. class ProductDataImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath = filepath @formatter = formatter end def import case File.extname(filepath) when ".csv" import_csv when ".xlsx" import_xlsx end end private def import_csv CSV.foreach(filepath, headers: true) do row formatted_data = formatter.build(row) formatted_data = formatter.build_csv(row) Product.create(formatted_data) end end def import_xlsx Xlsx.foreach(filepath) do row formatted_data = formatter.build_xlsx(row) Product.create(formatted_data) end end ... end johnschoeman
48. New Requirement New currency format $ johnschoeman
49. Commit 4 Allow users to upload csv of products Introduce product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer johnschoeman
50. RSpec.describe CsvImporter describe "#import" do it "saves every row in the file as new product" do filename = "products.csv" stub_csv(filename) formatter = ProductDataFormatter.new importer = CsvImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
51. class CsvImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath = filepath @formatter = formatter end def import CSV.foreach(filepath, headers: true) do row formatted_data = formatter.build_csv(row) Product.create(formatted_data) end end end johnschoeman
52. RSpec.describe XlsxImporter describe "#import" do it "saves every row in the file as new product" do filename = "products.xlsx" stub_xlsx(filename) formatter = ProductDataFormatter.new importer = XlsxImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
53. class XlsxImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath = filepath @formatter = formatter end def import Xlsx.foreach(filepath) do row formatted_data = formatter.build_xlsx(row) Product.create(formatted_data) end end end johnschoeman
54. Commit 5 Allow users to upload csv of products Introduce product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer johnschoeman
55. RSpec.describe FileImporter describe "#import" do it "raise if called" do importer = FileImporter.new("filepath", double("formatter")) expect { importer.import }.to raise_error("Must overwrite method") end end end johnschoeman
56. class FileImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath = filepath @formatter = formatter end def import raise "Must overwrite method" end end johnschoeman
57. class XlsxImporter < FileImporter ... end class CsvImporter < FileImporter ... end johnschoeman
58. Commit 6 Allow users to upload csv of products Introduce product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer Introduce data builder class johnschoeman
59. RSpec.describe ProductDataBuilder describe "#build" do it "raises" do formatter = double builder = ProductDataBuilder.new(formatter) expect { builder.build }.to raise_error("Must override method") end end end johnschoeman
60. class ProductDataBuilder attr_reader :formatter def initialize(formatter) @formatter = formatter end def build raise "Must override method" end end johnschoeman
61. RSpec.describe CsvBuilder do describe "#build" do context "when given a csv_row of product data" do it "creates a hash of the product data" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] csv_row = CSV::Row.new(headers, fields) formatter = ProductDataFormatter.new builder = CsvBuilder.new(formatter) result = builder.build(csv_row) expect(result).to eq({ ...data }) end end end end johnschoeman
62. class CsvBuilder < ProductDataBuilder def build(csv_row) data = csv_row.to_h.symbolize_keys formatter.format(data) end end johnschoeman
63. RSpec.describe XlsxBuilder do describe "#build" do context "when given a xlsx_row of product data" do it "creates a hash of the product data" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] xslx_row = stub_xslx_row(headers, fields) formatter = ProductDataFormatter.new builder = CsvBuilder.new(formatter) result = builder.build(xlsx_row) expect(result).to eq({ ...data }) end end end end johnschoeman
64. class XlsxBuilder < ProductDataBuilder HEADERS = %w[name author release_date value version active].freeze def build(xlsx_row) cells = xlsx_row.cells.map(&:value) data = HEADERS.zip(cells).to_h.symbolize_keys formatter.format(data) end end johnschoeman
65. Commit 7 Allow users to upload csv of products Introduce product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer Introduce data builder class Format currency data from csv johnschoeman
66. RSpec.describe ProductDataFormatter do ... context "when the currency has a dollar sign" do it "strips the dollar sign" do data = { value: "$1230" } formatter = ProductDataFormatter.new result = formatter.format(data) expect(result).to eq({ value: 1230 }) end end johnschoeman
67. class ProductDataFormatter def format(data) data[:title] = format_title(data[:title]) data[:release_date] = format_date(data[:release_date]) data[:value] = format_currency(data[:value]) data end ... def format_currency(input) input.to_s.gsub(/^\$/, "").to_i end end johnschoeman
68. And a New Requirement... johnschoeman
69. - app - models - application_record.rb - csv_builder.rb - csv_importer.rb - file_importer.rb - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
70. - spec - models - csv_builder_spec.rb - csv_importer_spec.rb - file_importer_spec.rb - product_spec.rb - product_data_builder_spec.rb - product_data_formatter_spec.rb - product_data_importer_spec.rb - xlsx_builder_spec.rb - xlsx_importer_spec.rb johnschoeman
71. Functional Path johnschoeman
72. New Requirement xlsx + csv johnschoeman
73. Commit 1 Allow users to upload csv of products Introduce product importer service johnschoeman
74. RSpec.describe ProductDataImporter do describe ".import" do it "takes a file and creates product data from the data" do filename = Rails.root.join("spec/fixtures/products.csv") importer = ProductDataImporter.new expected_data = { ...data } importer.import(filename) expect(Product.count).to eq 3 expect_product_to_match(Product.last, expected_data) end end johnschoeman
75. class ProductDataImporter def self.import(filepath) new.import(filepath) end def import(filepath) CSV.foreach(filepath, headers: true) do row data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.parse(data[:release_date] Product.create(data) end end end johnschoeman
76. Commit 2 Allow users to upload csv of products Introduce product importer service Introduce data pipeline johnschoeman
77. class ProductDataImporter def self.import(filepath) new.import(filepath) end def import(filepath) read_file(filepath). map { data process_data(data) }. map { data Product.create(data) } end private def read_file(filepath) CSV.foreach(filepath, headers: true).map do row row.to_h.symbolize_keys end end def process_data(data) process_date(data). then { data process_title(data) } end def process_date(data) new_data = data.dup new_data[:release_date] = Time.zone.parse(data[:release_date]) new_data end def process_title(data) new_data = data.dup new_data[:title] = title.titleize new_data end end johnschoeman
78. class ProductDataImporter ... def import(filepath) read_file(filepath). map { data process_data(data) }. map { data Product.create(data) } end private ... def process_data(data) process_date(data). then { data process_title(data) } end ... end johnschoeman
79. Commit 3 Allow users to upload csv of products Introduce product importer service Introduce data pipeline Allow for users to upload either xlsx or csv johnschoeman
80. RSpec.describe ProductDataImporter do ... context "when provided a xlsx file" do it "creates product data from the data" do filename = Rails.root.join("spec/fixtures/products.xlsx") importer = ProductDataImporter.new expected_data = { ...data } importer.import(filename) expect(Product.count).to eq 4 expect_product_to_match(Product.last, expected_data) end johnschoeman
81. class ProductDataImporter ... def import(filepath) read_file(filepath). map { data process_data(data) }. map { data Product.create(data) } end private def read_file(filepath) case File.extname(filepath) when ".csv" read_csv(filepath) when ".xlsx" read_xlsx(filepath) end end def read_xlsx(filepath) foreach_xlsx(filepath).map do row cells = row.cells.map(&:value) data = HEADERS.zip(cells).to_h.symbolize_keys end end ... end johnschoeman
82. New Requirement New currency format $ johnschoeman
83. Commit 4 Allow users to upload csv of products Introduce product importer service Introduce data pipeline Allow for users to upload either xlsx or csv Format currency data from csv johnschoeman
84. - spec - fixtures - products.csv - products.xlsx johnschoeman
85. class ProductDataImporter ... def process_data(data) process_date(data). then { data process_description(data) }. then { data process_currency(data) } end ... def process_currency(data) new_data = data.dup new_data[:value] = value.gsub(/^\$/, "").to_i new_data end end johnschoeman
86. Recap • Initial Requirement: Clients Import CSV • and then New Requirement: .xlsx + .csv • and then New Requirement: New currency format • and then a New Requirement... johnschoeman
87. Benefits of choosing the right paradigm for the task 1.Clearer Code 2.Less Code 3.Easier to Test and Maintain 4.Higher Development Velocity johnschoeman
88. 1. Clearer Code johnschoeman
89. - app - models - csv_builder.rb - csv_importer.rb - file_importer.rb - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
90. - spec - models - csv_builder_spec.rb - csv_importer_spec.rb - file_importer_spec.rb - product_spec.rb - product_data_builder_spec.rb - product_data_formatter_spec.rb - product_data_importer_spec.rb - xlsx_builder_spec.rb - xlsx_importer_spec.rb johnschoeman
91. - app - models - product.rb - services - product_data_importer.rb johnschoeman
92. - spec - fixtures - products.csv - products.xlsx - services - product_data_importer_spec.rb johnschoeman
93. 2. Less Code johnschoeman
94. Total Diff johnschoeman
95. Accumulated Diff johnschoeman
96. Easier to Test / Maintain Style Public APIs added OO 8 FP 1 johnschoeman
97. Higher Development Velocity Style Dev Time OO ~ A day FP ~ An hour johnschoeman
98. Paradigm Smells Using OO for a task that lends itself to FP 1.Lots of 'Something-er' Classes 2.UML is a Linked List johnschoeman
99. Lots of 'Something-er' Classes - app - models - csv_builder.rb - csv_importer.rb - file_importer.rb - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
100. UML is a Linked List johnschoeman
101. Take Aways johnschoeman
102. It's useful to consider how requirements might change johnschoeman
103. Sprinkles, just a little goes a long way johnschoeman
104. Be conscientious of business goals before starting work. johnschoeman
105. Action Item Before beginning a task, Ask how you expect requirements will change: Data or Behaviour? If Data, consider a functional style If Behavior, consider a object oriented style johnschoeman
106. Thesis Ruby is a multi-paradigm language. Different paradigms lend themselves to different tasks We should choose our style based off of our task johnschoeman
107. FP Learning Resources Gary Bernhardt - Functional Core, Imperative Shell thoughtbot.com/blog Piotr Solnica - Blending Functional and OO Programming in Ruby johnschoeman
108. Repo johnschoeman/sprinkles-of-functional-programming-app johnschoeman
109. Questions johnschoeman