DavidRaab

Replacing Higher-Order functions with data

Jan 26th, 2017
148
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
F# 5.60 KB | None | 0 0
  1. // Consider our domain logic is some kind of game with character with health and so on.
  2. type Player {
  3.     Id:     int
  4.     Name:   string
  5.     Health: int
  6. }
  7.  
  8. // Because we have a lot of those characters we have some kind of service that manages all of those characters
  9. // this service is able to find or search for any character. Now lets assume we want to create a function
  10. // that damage or heal a character, we could do it like this:
  11.  
  12. let heal service id amount =
  13.     let character = service.fetch(id)
  14.     {character with Health = (character.Health + amount)} // This returns a new character with Health changed
  15.  
  16. let damage service id amount =
  17.     let character = service.fetch(id)
  18.     {character with Health = (character.Health - amount)} // This returns a new character with Health changed
  19.  
  20. // "service" is an object that is able to fetch the character by its id. The whole idea is actually Dependency Injection.
  21. // We can switch out "service" with something different. But if we want to test "heal" or "damage" we still must provide
  22. // a service. Let's consider the default "service" is too hard to create and for some reason to wired with other systems.
  23. // What we want is another "service" that we just can provide some in-memory data that it returns. In the case of an object
  24. // we still need an interface and create a whole new class. But this isn't the only problem.
  25. //
  26. // We also need to change our code that somewhere that different class for fetching is used. One way would be mocking. So
  27. // whenever there is an instance of the original Service created now your new fake class is used. If you don't have mocking
  28. // avaiable then replacing it becomes even harder and a mess. One way would be to replace the "service" object by a
  29. // function
  30.  
  31. let heal fetchCharacter id amount =
  32.     let character = fetchCharacter id
  33.     {character with Health = (character.Health + amount)} // This returns a new character with Health changed
  34.  
  35. let damage fetchCharacter id amount =
  36.     let character = fetchCharacter id
  37.     {character with Health = (character.Health - amount)} // This returns a new character with Health changed
  38.  
  39. // Doesn't seems like a big difference, but now that we have a function that returns a character it is way more easier
  40. // to use that function with testing. We don't need to create a whole service class anymore, also no interfaces and so on
  41. // is needed. For testing we could directly create an inline function that just returns a character
  42.  
  43. let updatedChar = heal (fun _ -> {Id=10; Name="Foo"; Health=100}) 10 100
  44. Assert.Equal(updatedChar.Health, 200)
  45.  
  46. // Expecting functions instead of service classes means we can very easily replace the service class, no mocking is needed.
  47. // In general Testing becomes a lot easier.
  48. //
  49. // But another solution would be to remove the side-effect of fetching something completely from the "heal" or "damage" function
  50. // Instead of fetching and healing or fetching and damage, they just expect a character and just do they thing with it.
  51. //
  52. // The general rule: Don't pass things that get things, pass things directly.
  53. //
  54. // So the updated "heal" and "damage" functions looks like this:
  55.  
  56. let heal character amount =
  57.     {character with Health = (character.Health + amount)} // This returns a new character with Health changed
  58.  
  59. let damage character amount =
  60.     {character with Health = (character.Health - amount)} // This returns a new character with Health changed
  61.  
  62. // With those updates you also can test easily if your core logic works like intended
  63.  
  64. let updatedChar = heal {Id=10; Name="Foo"; Health=100} 100
  65. Assert.Equal(updatedChar, 200)
  66.  
  67. // There is only one thing now missing. Most of your application don't work by passing characters around. Your
  68. // application works by passing "id" around. So what you need is only one function with a side-effect. You
  69. // need a function that is able to fetch a character from your service. Or technically you can even
  70. // keep the previous "service" class if you want. So how does your code change?
  71.  
  72. // Previously you had a line of code like this:
  73.  
  74. heal service id 100
  75.  
  76. // In the above you got your service from somewhere. You also got your "id" from somewhere and you pass all things
  77. // to the heal function, this function had fetched the correct character and increased it health, and because of
  78. // this was harder to test.
  79. //
  80. // With the updated idea that "heal" and "damage" don't fetch the character anymore, now at this place you must first
  81. // fetch the character, and then heal it. Now your code changes to this:
  82.  
  83. let character = service.fetch(id)
  84. heal character 100
  85.  
  86. // You still have your "service" and just your "id". With those both you fetch your character. This character then can
  87. // be used with the "heal" function. But the advantage is. "heal" doesn't fetch anything from anywhere and is easily testable.
  88. // You also never need to change "service" or mock "service". Consider, the only reason why you ever wanted to mock "service"
  89. // was that you could test your "heal" or "damage" function. But the new functions don't depend on a "service" class.
  90. //
  91. // How does that change any Observable code, other kind of stuff? It don't change it all. You still can have Observables that
  92. // pass "id" around, and those "id" are async and so on. But instead of passing a "service" or a "fetchFunc" to another function
  93. // you just pass the result to it. In general you can say. If you have something like:
  94.  
  95. let func (fetch: id -> Thing) a b c =
  96.     ...
  97.  
  98. // you should replace it with
  99.  
  100. let func Thing a b c =
  101.     ...
  102.  
  103. // This way "func" becomes completely independent, free of any side-effects, and is easily testable.
Advertisement
Add Comment
Please, Sign In to add comment