Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Web with Hanami

76aa5b311fb40fdc2bb70c0282d66af3?s=47 Luca Guidi
September 23, 2017

Functional Web with Hanami

Web applications are complex. MVC helps you to structure them, but you're still you're struggling with tangled dependencies, difficult reusability, byzantine patterns, hard to maintain model domains?

What if you can have small, simple, and deterministic components that can be easily used (and reused) in your web apps?

Let's see how functions, FP/OOP blending, data transformations, immutability, Dependency Injection can help to solve these problems.

With this talk you'll learn how Hanami makes these principle easy to use, and what's the future of this uprising Ruby web framework.

76aa5b311fb40fdc2bb70c0282d66af3?s=128

Luca Guidi

September 23, 2017
Tweet

Transcript

  1. RailsClub 2017 // @jodosha Functional Web with Hanami

  2. Привет, Москва " MAXBORN

  3. Luca Guidi https://lucaguidi.com @jodosha MAXBORN

  4. Hanami http://hanamirb.org @hanamirb MAXBORN

  5. MAXBORN https://dnsimple.com @dnsimple DNSimple

  6. Stickers!!!

  7. RailsClub 2017 // @jodosha Functional Web with Hanami

  8. What is a function?

  9. It’s an atomic set of instructions that (may) accept an

    input and (may) return an output.
  10. A function is a task.

  11. A function is the building block of our apps.

  12. clear_database() # => nil Date.today() # => 2017-09-23 delete_file("foo.txt") #

    => nil twice(2) # => 4
  13. What is a pure function?

  14. It’s a function where the output is only determined by

    its input.
  15. 15 No side effects

  16. 16 Pop quiz

  17. Pure or not? pure sum(2, 2)

  18. Pure or not? not pure Time.now

  19. Pure or not? pure "hanami".capitalize

  20. Pure or not? not pure r = BookRepository.new r.create(title: "Hanami")

  21. Pure or not? not pure c = ApiClient.new c.login("jodosha", "*******")

  22. 22 Pure functions have no side effects and they are

    deterministic.
  23. $ shasum -a 256 file.txt 7fdd629e6366b7c758e1b823…

  24. Pure or not? not pure def file_hash(path) Digest::SHA256.file(path).hexdigest end

  25. Why is so much important to draw a line between

    pure and non-pure functions?
  26. Because non-pure functions are unpredictable.

  27. file_hash("testfile") # => Errno::ENOENT: # No such file or directory

    # @ rb_sysopen - testfile
  28. Which kind of functions are easier to reason about and

    to test?
  29. Pure!!!

  30. it "returns 2 + 2" do expect(sum(2, 2)).to be(4) end

    Pure
  31. Non-pure (1 of 4) before do touch(testfile) end let(:testfile) do

    "path/to/testfile" end
  32. Non-pure (2 of 4) it "returns a hash for a

    given file" do # ... end
  33. Non-pure (3 of 4) it "returns a different hash for

    another file" do append(testfile, "lorem ipsum..") # ... end
  34. Non-pure (4 of 4) it “raises an error when file

    is missing" do delete(testfile) # ... end
  35. before do touch(testfile) end let(:testfile) { "path/to/testfile" } it "returns

    a hash for a given file" do # ... end it "returns a different hash for another file” do append(testfile, "lorem ipsum..") # ... end it "raises an error when file is missing" do delete(testfile) # ... end Pure vs non-pure it "returns 2 + 2" do expect(sum(2, 2)).to be(4) end
  36. 36 Pure functions require less setup and less specs.

  37. 37 Non-pure functions are harder to test.

  38. 38

  39. Which kind of functions would you like to see more

    in your apps? Pure or non- pure?
  40. Pure!!!

  41. String#capitalize string = "hello world" string.capitalize # => "Hello world"

    string.capitalize # => "Hello world" string.capitalize # => "Hello world"
  42. Deterministic (1 of 2) class Formatter def initialize(string) @string =

    string end def capitalize @string.capitalize end end
  43. Deterministic (2 of 2) formatter = Formatter.new("hello world") formatter.capitalize #

    => "Hello world" formatter.capitalize # => "Hello world" formatter.capitalize # => "Hello world"
  44. Non-deterministic (1 of 2) class Formatter # ... def replace(other)

    @string.replace(other) end end
  45. Non-deterministic (2 of 2) formatter = Formatter.new("hello world") formatter.capitalize #

    => "Hello world" formatter.replace("foo") formatter.capitalize # => "Foo" ⚠⚠⚠
  46. 46 Mutable state breaks the deterministic nature of our functions.

  47. 47

  48. class User < ORM def initialize(attributes) @attributes = attributes end

    def capitalize @attributes.fetch(:name).capitalize end def update_attributes(other) @attributes.merge!(other) end end
  49. Hanami (1 of 3) class User < Hanami::Entity def capitalize

    name.capitalize end end repository = UserRepository.new
  50. Hanami (2 of 3) # create user = repository.create(params[:user]) user.id

    # => 1 user.capitalize # => "Luca" user.capitalize # => "Luca" user.capitalize # => "Luca"
  51. Hanami (3 of 3) # update user = repository.update( params[:id],

    params[:user]) user.id # => 1 user.capitalize # => “Luca Guidi" user.capitalize # => "Luca Guidi" user.capitalize # => "Luca Guidi”
  52. Recap * Pure functions * Immutability

  53. 53

  54. A function is the building block of our apps.

  55. 55 The most important feature for Lego bricks is compatibility.

  56. clear_database() # => nil Date.today() # => 2017-09-23 delete_file("foo.txt") #

    => nil twice(2) # => 4
  57. def add(a, b) a + b end ->(a, b) {

    a + b } Sum.new Sum
  58. 58 Callable Objects

  59. ->(a, b) { a + b }.call(2, 2) # =>

    4 method(:add).call(2, 2) # => 4 Sum.new.call(2, 2) # => 4 Sum.call(2, 2) # => 4
  60. ->(a, b) { a + b }.call(2, 2) # =>

    4 method(:add) .call(2, 2) # => 4 Sum.new .call(2, 2) # => 4 Sum .call(2, 2) # => 4
  61. ["hello", "world"].map(&:upcase) # => [“HELLO", “WORLD"]

  62. sum = ->(a, b) { a + b } sum.call(1,

    2) # => 3 (1..20).inject(&sum) # => 210
  63. multi = ->(a, b) { a * b } multi.(2,

    3) # => 6 pow = ->(a, n) do n.times.inject(1) do |memo| memo = multi.(memo, a) end end pow.(2, 5) # => 32 pow.(2, 0) # => 1 # quadratic equations...
  64. 64 We can make our logic (objects) to become “higher

    order functions”.
  65. class CompareNumbers def call(a, b) a <=> b end def

    to_proc method(:call).to_proc end end CompareNumbers.new.call(23, 11) # => 1 [23, 11, 99].sort(&CompareNumbers.new) # => [11, 23, 99]
  66. 66

  67. 67 Compatibility Reusability Small and focused objects

  68. Recap * Pure functions * Immutability * Callable Objects

  69. 69 The big picture

  70. What is an application?

  71. It’s a set of features.

  72. What is a feature?

  73. A feature it’s a task for our users.

  74. A function is a task.

  75. A feature is a big function made of smaller functions.

  76. How to structure a feature?

  77. MVC?

  78. 78 V C MVC

  79. 79 Identify and structure common tasks.

  80. What are the common tasks for a feature?

  81. 1. Route the request 2. Verify the request (verb, MIME)

    3. Authentication 4. Coerce & validate input 5. Transform input 6. Verify preconditions (rules) 7. CRUD 8. Trigger postconditions 9. Present the output
  82. 82 Task Component Route the request Hanami::Router HTTP verb/MIME Hanami::Action

    Authentication (custom) Coerce & validate Hanami::Validations Transform (missing) Preconditions (custom) CRUD Hanami::Repository Postconditions (missing) Present Hanami::View
  83. 83 Task Component Func? Route the request Hanami::Router ✅ yes

    HTTP verb/MIME Hanami::Action ❌ no Authentication (custom) ❗not enforced Coerce & validate Hanami::Validations ❌ no Transform Hanami::Transformer in progress Preconditions (custom) ❗not enforced CRUD Hanami::Repository ✅ yes Postconditions Hanami::Events in progress Present Hanami::View ❌ no
  84. 84

  85. Current design class MyValidator include Hanami::Validations validations do required(:name).filled(:str?) end

    end result = MyValidator.new(name: "Luca").validate result.successful? # => true
  86. Future design class MyValidator < Hanami::Validator validations do required(:name).filled(:str?) end

    end result = MyValidator.new.call(name: "Luca") result.successful? # => true
  87. Self-registering (1 of 3) UserRepository.new => "users" WelcomeMailer.new => "mailers.welcome"

    SignupValidator.new => "validators.signup"
  88. Self-registering (2 of 3) module Web::Controllers::Users class Create include Web::Action

    def call(params) UserRepository.new.create(params[:user]) end end end
  89. Self-registering (3 of 3) module Web::Controllers::Users class Create < Hanami::Action

    import :users def call(params) users.create(params[:user]) end end end
  90. 90 Self-registration is possible via a fine tuned mechanism, which

    is able to resolve a tree of dependencies and to inject them.
  91. Recap * Pure functions * Immutability * Callable Objects *

    Dependency Injection
  92. 92 Task Pure? Route the request ✅ yes HTTP verb/MIME

    ✅ yes Authentication ✅ yes Coerce & validate ✅ yes Transform ✅ yes Preconditions ✅ yes CRUD ❌ no* Postconditions ❌ no* Present ✅ yes
  93. 93 Task Pure? MVC Route the request ✅ yes Controller

    HTTP verb/MIME ✅ yes Controller Authentication ✅ yes Controller Coerce & validate ✅ yes Model Transform ✅ yes Model Preconditions ✅ yes Model CRUD ❌ no* Model Postconditions ❌ no* Model Present ✅ yes View
  94. 94 V C MVC

  95. How to structure a feature?

  96. Pipeline

  97. 97 A pipeline is a set of compatible components that

    are executed in a specific order. If one of those steps returns an error, the execution is stopped.
  98. 1. Route the request 2. Verify the request (verb, MIME)

    3. Authentication 4. Coerce & validate input 5. Transform input 6. Verify preconditions (rules) 7. CRUD 8. Trigger postconditions 9. Present the output
  99. Pipeline (1 of 2) class Signup < Hanami::Interactor import :validator,

    :transformer, :users pipeline :validator, :transformer, :persist, :notify private def persist(input:, output:) output[:user] = users.create(input) end def notify(output:, **) events.broadcast(“signup", user_id: output[:user].id) end end
  100. Pipeline (2 of 2) module Web::Controllers::Users class Create < Hanami::Action

    import :signup def call(params) result = signup.call(params[:signup]) if result.successful? # ... else # ... end end end end
  101. 101

  102. Recap * Pure functions * Immutability * Callable Objects *

    Dependency Injection * Pipelines
  103. Спасибо! https://lucaguidi.com @jodosha MAXBORN